Compare commits

..

5 Commits

Author SHA1 Message Date
b4fdc6e813 optimize rendering 2026-02-09 09:24:50 +09:00
b22bc95ad5 add bench 2026-02-09 09:24:50 +09:00
d16b17b4c8 fix kouho selection. 2026-02-09 09:24:50 +09:00
d5d3c23f3e vietnamese telex input 2026-02-09 09:24:50 +09:00
032c60e193 add lang dispatch, korean hangul input 2026-02-09 09:24:45 +09:00
233 changed files with 169190 additions and 501096 deletions

78
.gitignore vendored
View File

@@ -1,21 +1,57 @@
/*.o
/strans
/imv2.c
/imv2.h
/vkv1.c
/vkv1.h
/tests/*.o
/tests/unit_test
/tests/stress_test
/tests/ibus_live_test
/tests/ibus_client_smoke
/tests/gtk_live_test
/tests/xim_live_test
/tests/ipc_live_test
/tests/daemon_collision_test
/tests/daemon_failure_test
/tests/daemon_restart_test
/gtk/im-strans.so
/bench/bench
/bench/perf.data*
/ref-*/
strans
strans-xim
# Prerequisites
*.d
# Object files
*.o
*.ko
*.obj
*.elf
# Linker output
*.ilk
*.exp
# Precompiled Headers
*.gch
*.pch
# Libraries
*.lib
*.a
*.la
*.lo
# Shared objects (inc. Windows DLLs)
*.dll
*.so
*.so.*
*.dylib
# Executables
*.exe
*.out
*.app
*.i*86
*.x86_64
*.hex
# Debug files
*.dSYM/
*.su
*.idb
*.pdb
# Kernel Module Compile Results
*.mod*
*.cmd
.tmp_versions/
modules.order
Module.symvers
Mkfile.old
dkms.conf
# debug information files
*.dwo

3
.gitmodules vendored
View File

@@ -1,3 +0,0 @@
[submodule "cutest"]
path = cutest
url = ../cutest.git

View File

@@ -1,50 +0,0 @@
FROM archlinux:base@sha256:b0deabeb3d283da2c7f7dbf0eea051b7b2cd0554e0b737cc457fd21683bdcdd1
RUN pacman -Syu --noconfirm --needed \
cairo \
fontconfig \
diffutils \
gcc \
gtk3 \
ibus \
harfbuzz \
libx11 \
libxcb \
libxkbcommon \
make \
noto-fonts-emoji \
pango \
pkgconf \
plan9port \
python \
ttf-dejavu \
ttf-jigmo \
valgrind \
wayland \
which \
xorg-server-xvfb \
xcb-imdkit \
xcb-util \
dbus \
&& pacman -Scc --noconfirm
RUN export DEBUGINFOD_URLS=https://debuginfod.archlinux.org \
&& buildid="$(readelf -n /usr/lib/ld-linux-x86-64.so.2 | sed -n 's/.*Build ID: //p')" \
&& test -n "$buildid" \
&& debugdir="/usr/lib/debug/.build-id/$(printf '%.2s' "$buildid")" \
&& mkdir -p "$debugdir" \
&& install -m 0644 "$(debuginfod-find debuginfo /usr/lib/ld-linux-x86-64.so.2)" \
"$debugdir/${buildid#??}.debug" \
&& rm -rf /root/.cache/debuginfod_client
RUN dbus-uuidgen --ensure=/etc/machine-id
ENV PLAN9=/usr/lib/plan9
ENV PATH=$PATH:/usr/lib/plan9/bin
# 9c and 9l filter their own output through $egrep, which plan9port never
# assigns, so the pipeline dies and takes every warning and error with it.
# Both source $PLAN9/config, which the distribution does not ship.
RUN echo 'egrep="grep -E"' > $PLAN9/config
WORKDIR /src

View File

@@ -1,26 +0,0 @@
Copyright (c) 2005,2006 Choe Hwanjin
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the author nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.

View File

@@ -1,339 +0,0 @@
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.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
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.
<signature of Ty Coon>, 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.

View File

@@ -1,17 +0,0 @@
# License inventory
- `BSD-3-Clause-libhangul-hanja.txt` contains the terms for the Hanja data
in `map/hanja.src` and the symbol data in `map/mssymbol.src`, and for the
generated `map/hanja.dict` they share, derived from libhangul's
`data/hanja/hanja.txt` and `data/hanja/mssymbol.txt`.
- `GPL-2.0-or-later.txt` contains GNU GPL version 2. The header of
`map/kanji.dict` grants the option to use GPL version 2 or any later
version.
- `Unicode-3.0.txt` contains the Unicode License v3, the terms for the
emoji names and keywords in `map/emoji.src` and generated
`map/emoji.dict`, derived from Unicode's `emoji-test.txt` and CLDR's
annotations.
- The two protocol descriptions in `proto/` are vendored verbatim from
wlroots `012ca825` and carry their own MIT terms inside the XML.
No repository-wide license has been declared for original strans source.

View File

@@ -1,39 +0,0 @@
UNICODE LICENSE V3
COPYRIGHT AND PERMISSION NOTICE
Copyright © 1991-2026 Unicode, Inc.
NOTICE TO USER: Carefully read the following legal agreement. BY
DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR
SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE
TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT
DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE.
Permission is hereby granted, free of charge, to any person obtaining a
copy of data files and any associated documentation (the "Data Files") or
software and any associated documentation (the "Software") to deal in the
Data Files or Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, and/or sell
copies of the Data Files or Software, and to permit persons to whom the
Data Files or Software are furnished to do so, provided that either (a)
this copyright and permission notice appear with all copies of the Data
Files or Software, or (b) this copyright and permission notice appear in
associated Documentation.
THE DATA FILES AND SOFTWARE ARE 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 OF
THIRD PARTY RIGHTS.
IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE
BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES,
OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA
FILES OR SOFTWARE.
Except as contained in this notice, the name of a copyright holder shall
not be used in advertising or otherwise to promote the sale, use or other
dealings in these Data Files or Software without prior written
authorization of the copyright holder.

115
Makefile
View File

@@ -1,118 +1,27 @@
CC = 9c
LD = 9l
PKG_CONFIG ?= pkg-config
SCANNER ?= wayland-scanner
CFLAGS ?= -O2 -g
WARN_CFLAGS = -Wall -Wextra
IBUS_CFLAGS = $(shell $(PKG_CONFIG) --cflags dbus-1 xkbcommon)
IBUS_LIBS = $(shell $(PKG_CONFIG) --libs dbus-1 xkbcommon)
XIM_CFLAGS = $(shell $(PKG_CONFIG) --cflags xcb-imdkit xcb-aux xcb-xkb xkbcommon-x11)
XIM_LIBS = $(shell $(PKG_CONFIG) --libs xcb-imdkit xcb-aux xcb-xkb xkbcommon-x11)
TEXT_CFLAGS = $(shell $(PKG_CONFIG) --cflags pangocairo cairo fontconfig)
TEXT_LIBS = $(shell $(PKG_CONFIG) --libs pangocairo cairo fontconfig)
POPUP_CFLAGS = $(shell $(PKG_CONFIG) --cflags xcb-aux xcb-randr)
POPUP_LIBS = $(shell $(PKG_CONFIG) --libs xcb-aux xcb-randr)
WL_CFLAGS = $(shell $(PKG_CONFIG) --cflags wayland-client xkbcommon)
WL_LIBS = $(shell $(PKG_CONFIG) --libs wayland-client xkbcommon)
PROJECT_CPPFLAGS =
PROJECT_LDLIBS = -lthread -lbio -lxcb $(IBUS_LIBS) $(TEXT_LIBS) \
$(XIM_LIBS) $(POPUP_LIBS) $(WL_LIBS)
CFLAGS = -Wall -Wextra -O2 -g
PROG = strans
DOCKER_IMAGE = strans-build
DOCKER_RUN = docker run --rm --user "$$(id -u):$$(id -g)" \
-v "$(CURDIR):/src" $(DOCKER_IMAGE)
SRCS = compose.c dict.c font.c ibus.c ipc.c ko.c main.c popup_layout.c \
srv.c str.c strans.c trie.c vi.c win.c wl.c xim.c
SRCS = $(wildcard *.c)
OBJS = $(SRCS:.c=.o)
# wayland-scanner writes these from the XML in proto/; only the XML is tracked.
PROTO = imv2.c imv2.h vkv1.c vkv1.h
PROTOOBJ = imv2.o vkv1.o
all: $(PROG) gtk
all: $(PROG) xim bench
$(PROG): $(OBJS) $(PROTOOBJ)
$(LD) $(LDFLAGS) -o $@ $(OBJS) $(PROTOOBJ) $(PROJECT_LDLIBS) $(LDLIBS)
$(PROG): $(OBJS)
$(LD) -o $@ $(OBJS) -lthread -lString -lbio -lxcb -lm
$(OBJS): dat.h fn.h ipc.h
wl.o: imv2.h vkv1.h
compose.o ibus.o: PROJECT_CPPFLAGS += $(IBUS_CFLAGS)
font.o: PROJECT_CPPFLAGS += $(TEXT_CFLAGS)
win.o: PROJECT_CPPFLAGS += $(POPUP_CFLAGS)
wl.o $(PROTOOBJ): PROJECT_CPPFLAGS += $(WL_CFLAGS)
xim.o: PROJECT_CPPFLAGS += $(XIM_CFLAGS)
%.o: %.c
$(CC) $(CPPFLAGS) $(PROJECT_CPPFLAGS) $(WARN_CFLAGS) $(CFLAGS) -c -o $@ $<
imv2.h: proto/input-method-unstable-v2.xml
$(SCANNER) client-header $< $@
imv2.c: proto/input-method-unstable-v2.xml
$(SCANNER) private-code $< $@
vkv1.h: proto/virtual-keyboard-unstable-v1.xml
$(SCANNER) client-header $< $@
vkv1.c: proto/virtual-keyboard-unstable-v1.xml
$(SCANNER) private-code $< $@
clean:
rm -f $(OBJS) $(PROG) $(PROTO) $(PROTOOBJ)
$(MAKE) -C tests clean
$(MAKE) -C gtk/ clean
$(MAKE) -C bench/ clean
rm -f $(OBJS) $(PROG)
make -C xim/ clean
make -C bench/ clean
gtk:
$(MAKE) -C gtk/
xim:
make -C xim/
bench:
$(MAKE) -C bench/
make -C bench/
docker-image: Dockerfile
docker build -t $(DOCKER_IMAGE) - < Dockerfile
docker-build:
$(DOCKER_RUN) sh -c 'make clean && make all'
docker-check:
$(DOCKER_RUN) sh -c 'make clean && \
make check check-live check-stress UNITARGS="$(UNITARGS)"'
docker-check-live:
$(DOCKER_RUN) sh -c 'make clean && make check-live'
docker-check-stress:
$(DOCKER_RUN) sh -c 'make clean && make check-stress'
docker-bench:
$(DOCKER_RUN) sh -c 'make clean && make bench'
docker-valgrind:
$(DOCKER_RUN) sh -c 'make clean && make -C tests unit_test && \
cd tests && valgrind -q --error-exitcode=1 ./unit_test $(UNITARGS)'
docker: docker-image
$(MAKE) docker-build
check: verify-map
$(MAKE) -C tests check UNITARGS="$(UNITARGS)"
check-live: $(PROG) gtk
$(MAKE) -C tests check-live
check-stress: $(PROG)
$(MAKE) -C tests check-stress
test: check
verify-map:
test "$$(cut -f1 map/hira.map)" = "$$(cut -f1 map/kata.map)"
python3 map/mktelex.py | cmp - map/telex.map
python3 map/mkemoji | cmp - map/emoji.dict
python3 map/mkhanja | cmp - map/hanja.dict
python3 map/verifymap.py map/*.map map/*.dict
python3 tests/mkemoji_test.py
python3 tests/mkhanja_test.py
python3 tests/skk2ktrans_test.py
.PHONY: all check check-live check-stress test verify-map clean gtk bench \
docker docker-image docker-build docker-check docker-check-live \
docker-check-stress docker-bench docker-valgrind
.PHONY: all clean xim bench

218
README.md
View File

@@ -1,194 +1,70 @@
# strans
strans is a small, single-user input method for Korean, Japanese, English,
emoji and symbols, with Vietnamese Telex as a compatibility mode. One
engine serves four frontends: `zwp_input_method_v2` on Wayland — sway and
any other compositor that offers it — IBus, which GTK 4 and Qt use, XIM,
and a GTK 3 module.
An input method daemon for CJK text entry on X11.
## Modes
Inspired by 9front's ktrans. Threads communicate via CSP channels.
| Key | Mode |
| --- | --- |
| `Ctrl+S` | Korean Hangul (2-beolsik) |
| `Ctrl+N` | Japanese Hiragana, converting to Kanji |
| `Ctrl+K` | Japanese Katakana |
| `Ctrl+T` | English |
| `Ctrl+V` | Vietnamese Telex |
| `Ctrl+E` | Emoji and symbol search |
| `Ctrl+H` | One-shot Hanja and symbol search |
## Dependencies
Only a plain `Ctrl` chord is a strans key: with `Shift`, `Alt` or `Super`
held it commits what is pending and goes to the application, so
`Ctrl+Shift+V` still pastes. `Backspace`, `Enter`, `Tab`, `Esc` and the
arrow and page keys do the same under `Ctrl`, so `Ctrl+Backspace` still
deletes a word. The mode switched to — `한`, `あ`, `ア`, `ă`, `A` — shows
in the popup until the next key. A Korean keyboard's 한/영 and 한자 keys
stand for `Ctrl+S`/`Ctrl+T` and `Ctrl+H`; a Japanese one's 半角/全角,
ひらがな/カタカナ, 変換 and 無変換 for the kana modes, `Space` and English.
- plan9port
- gtk+-3.0 (optional, for GTK IM module)
## Composing
## Build
Japanese composes a whole reading and then offers its Kanji with none
chosen. `Space` and `Tab` step through the candidates and wrap (`Shift`
reverses), the reading in Katakana last, so a word the dictionary lacks
converts with one `Space`; `Up`/`Down` and `PageUp`/`PageDown` move without
wrapping. A chosen candidate takes the reading's place in the preedit, so
it stands where it will land, and `Enter` commits it, or the reading, as
`0` and typing on always do. Once a candidate is chosen `1`-`9` take that
row of the page shown; before that the digits type. `Backspace` deletes
the last kana shown, and romaji that made no kana stays as typed until it
is mended; `Esc` cancels the reading, and on a chosen candidate both go
back to it. The dictionary keys verbs and adjectives by stem and
okurigana, so `kaku` offers かく's readings and then 書く and its kin —
typing the okurigana with `Shift`, as SKK does, `kaKu`, asks for that
split first. Katakana mode does not convert, so there `Space` and `Tab`
commit the reading and go on to the application.
Korean composes one syllable at a time. `Enter`, `Tab`, `Esc` and any key
that is not a jamo commit the syllable and go on to the application, so
`Esc` still leaves insert mode. Two lone consonants join into the compound
final they make (rt → ㄳ), and a vowel typed before its consonant is
reordered under it (kr → 가), as in libhangul.
A search takes the keys until `Enter`, `Space` or `1`-`9` picks a result or
`Esc` cancels, then returns to the previous mode. Emoji matches the typed
keys, and what they spell in the current mode, against a prefix of every
emoji's CLDR name and keywords in English, Korean and Japanese, and against
ASCII aliases such as `->` and `<=`. `Ctrl+H` takes the syllable being
composed as its query and composes on from it, converting a word as well as
a syllable — 한자 gives 漢字, 대한민국 gives 大韓民國 — and `Esc` gives the
syllable back. A word is committed a syllable at a time, so the reading
also reaches back into the text the application already holds, as far as
the dictionary knows the whole of it: type 한자 and then `Ctrl+H` and the
query is 한자, not 자, and picking 漢字 takes the 한 back. `Backspace`
gives that reach back before it deletes what was typed, so 상 written
with 태 typed answers 狀態, and one `Backspace` answers 態 for the
syllable alone. A reading is also the start of the longer words it
begins, whose conversions follow its own, so 대한 answers 大寒 first and
大韓民國 further down. A lone consonant is a reading too, and answers
with the symbol table a Korean keyboard's 한자 key has always offered:
ㅁ gives ※ ○ △ ㈜, ㄴ the brackets 「」『』, ㄹ the units ℃ ㎏ , ㅇ the
circled numbers ①②③.
Reaching back needs the application to hand over the text around its
cursor and to take some of it away again. The Wayland input method, IBus
and the GTK 3 module all can; XIM has no such request, so there the
reading is what is still being composed and nothing else, and an IBus
client that reads a key's effects only after the call returns is not
asked for its text either, since a deletion could not be ordered before
the commit.
Dead keys and Compose sequences are composed by strans itself for the
Wayland, XIM and IBus frontends, from `XCOMPOSEFILE` or the locale; the
GTK 3 module leaves them to GtkIMContextSimple.
## Preedit and candidates
| Frontend | Preedit | Candidates | Reaches back |
| --- | --- | --- | --- |
| Wayland input method | inline in the client | popup | yes |
| GTK 3 and IBus | inline in the client | popup | yes |
| XIM PreeditCallbacks | inline through XIM callbacks | popup | no |
| XIM PreeditPosition, PreeditNothing | popup | popup | no |
On Wayland the popup is a surface the compositor places at the text cursor,
flipping it above the line when there is no room below. Elsewhere it is an
X11 window, placed from the XIM spot or from the GTK caret, and XIM text
travels as `COMPOUND_TEXT`. Either way `GDK_SCALE` sizes it for HiDPI
displays.
## Build and test
```sh
git submodule update --init
make docker-image
make docker-build
```
That leaves `strans`, the daemon, and `gtk/im-strans.so`, the GTK 3 module.
Tests come in three tiers:
```sh
make check # generated-map validation and unit tests
make check-live # one IBus, GTK, XIM and IPC daemon smoke each
make check-stress # randomized, capacity, collision, failure and restart
```
`make docker-check` runs all three in the container, and
`make docker-valgrind` the unit suite under Valgrind; rerun
`make docker-image` after changing `Dockerfile`. `UNITARGS=hangul` filters
the unit suite alone, not the other tiers.
A native build wants a C toolchain, Make, pkg-config and Plan 9 port, plus
development files for D-Bus, XCB and xcb-imdkit, Wayland and
`wayland-scanner`, xkbcommon, Pango and Cairo, and GTK 3; the tests also
want Xvfb, Python 3 and fonts covering Latin, CJK and emoji.
[`Dockerfile`](Dockerfile) names the exact packages.
make
cd xim && make # XIM adapter
cd gtk && make docker && make install # GTK IM module
## Run
```sh
./run.sh # restart the daemon in the background
./strans map # or run it in the foreground
```
./strans map font &
`./strans DIR` reads `hira.map`, `kata.map`, `telex.map`, `kanji.dict`,
`emoji.dict` and `hanja.dict` from `DIR` at runtime; nothing but the GTK
module is installed. A service supervisor wants the session's
`XDG_RUNTIME_DIR` and its `WAYLAND_DISPLAY` or `DISPLAY`: the GTK module
finds the daemon at `$XDG_RUNTIME_DIR/strans.sock`, else
`/tmp/strans.UID`, and IBus clients through the address file libibus
expects under `~/.config/ibus/bus/`, which strans writes as `ibus-daemon`
would.
For XIM apps:
strans shows one popup per session, so it picks a frontend at startup: a
compositor offering `zwp_input_method_v2` gets the Wayland frontend, and
then neither XIM nor the X11 popup runs; every other session gets XIM. The
IBus endpoint and the GTK 3 module are served in both.
./xim/strans-xim &
XMODIFIERS=@im=strans xterm
On Wayland, then, set none of the variables below. GTK, Qt and Firefox
speak text-input-v3 themselves — GTK 4 binds it with `GTK_IM_MODULE` unset
— and setting these is what pushes them off the path that works: such a
client still composes inline but shows no candidates, because the popup
belongs to the Wayland frontend. Chromium is beyond help either way, since
it asks for `text-input-v1`, which wlroots does not implement.
For GTK apps:
```sh
export XMODIFIERS=@im=strans # XIM
doas make -C gtk install # GTK 3 module
export GTK_IM_MODULE=strans
GLFW_IM_MODULE=ibus kitty # IBus client example
```
GTK_IM_MODULE=strans gedit
strans provides its own IBus endpoint, so `ibus-daemon` and fcitx are not
needed. Building the GTK module wants GTK development files; installing
one built elsewhere does not. The install target takes `GTK_MODULE_DIR`
when set, else asks GTK where its modules live; `doas make -C gtk uninstall`
removes it. With neither `WAYLAND_DISPLAY` nor `DISPLAY` the daemon still
serves IBus and its socket, but nothing draws a popup.
## Usage
## Dictionary data
Switch input modes with Ctrl + key:
After changing an input source, regenerate and verify:
N Hiragana
K Katakana
S Hangul
T English
V Vietnamese (Telex)
E Emoji
```sh
python3 map/mktelex.py >map/telex.map
map/mkemoji >map/emoji.dict
map/mkhanja >map/hanja.dict
make verify-map
```
Type romanized input. Select candidates with 1-9 or arrow keys.
Tab or Enter to commit.
[`map/README`](map/README) says where the emoji, Hanja and Japanese data
comes from.
## Architecture
## Benchmark
Four threads communicate via CSP channels:
`make bench` builds `bench/bench`. With the daemon stopped, `./bench.sh`
starts one, warms it up and records the workload with Linux `perf` into
`bench/perf.data`.
- [imthread](strans.c#L271): keystroke processing, transliteration
- [dictthread](dict.c#L38): dictionary lookup
- [drawthread](win.c#L133): preedit window rendering
- [srvthread](srv.c#L42): IPC via unix socket
## Licensing
Adapters (strans-xim, im-strans.so) bridge X11/GTK events.
Third-party notices are in [`LICENSES`](LICENSES). The repository declares
no license for the strans source as a whole.
## Files
strans.c input method engine
dict.c dictionary queries
win.c xcb window management
font.c truetype rendering (stb_truetype)
map/ transliteration tables
font/ bundled CJK fonts
## References
- https://git.9front.org/plan9front/plan9front/HEAD/info.html
- https://np.mkv.li/kor/9front-ktrans-%ED%95%9C%EA%B8%80-%EB%98%91%EB%B0%94%EB%A1%9C-%EB%A7%8C%EB%93%A4%EA%B8%B0/
- https://en.wikipedia.org/wiki/Telex_(input_method)
- https://gist.github.com/hieuthi/0f5adb7d3f79e7fb67e0e499004bf558

View File

@@ -1,71 +1,23 @@
#!/bin/sh
set -eu
cd "$(dirname "$0")" || exit 1
if test "$#" -ne 0; then
echo "usage: bench.sh" >&2
exit 1
fi
if ! test -x ./bench/bench; then
echo "bench.sh: ./bench/bench is not executable; run make bench first" >&2
exit 1
fi
strans_pid=
perf_pid=
cleanup()
{
status=$?
trap - 0 1 2 15
if test -n "$perf_pid"; then
kill -INT "$perf_pid" 2>/dev/null || :
wait "$perf_pid" 2>/dev/null || :
fi
if test -n "$strans_pid"; then
kill "$strans_pid" 2>/dev/null || :
wait "$strans_pid" 2>/dev/null || :
fi
exit "$status"
}
trap cleanup 0
trap 'exit 129' 1
trap 'exit 130' 2
trap 'exit 143' 15
./strans map &
strans_pid=$!
# The daemon is ready once the benchmark client can complete a round.
attempt=0
until ./bench/bench bench/bench.keys 1 >/dev/null 2>&1; do
if ! kill -0 "$strans_pid" 2>/dev/null; then
echo "bench.sh: daemon exited before accepting IPC clients" >&2
exit 1
fi
attempt=$((attempt + 1))
if test "$attempt" -ge 50; then
echo "bench.sh: daemon did not accept IPC clients within 10 seconds" >&2
exit 1
fi
sleep 0.2
done
# warm up the renderer
./bench/bench bench/bench.keys 100
echo "renderer warmed up"
perf record -g -o bench/perf.data -p "$strans_pid" &
perf_pid=$!
pkill strans
pkill strans-xim
sleep 1
./strans map font &
sleep 1
# warm up glyph cache
./bench/bench bench/bench.keys 100
echo "cache warmed up"
STRANS_PID=$(pgrep -x strans)
perf record -g -o bench/perf.data -p "$STRANS_PID" &
PERF_PID=$!
sleep 1
kill -0 "$perf_pid"
./bench/bench bench/bench.keys 100
kill -INT "$perf_pid"
wait "$perf_pid" 2>/dev/null || :
perf_pid=
kill -INT $PERF_PID
wait $PERF_PID 2>/dev/null
pkill strans

View File

@@ -1,9 +1,8 @@
CC = cc
CFLAGS ?= -O2
CFLAGS = -Wall -O2
bench: main.c ../ipc.c ../ipc.h
$(CC) $(CPPFLAGS) -I.. -Wall $(CFLAGS) $(LDFLAGS) -o $@ \
main.c ../ipc.c $(LDLIBS)
bench: main.c
$(CC) $(CFLAGS) -o $@ main.c
clean:
rm -f bench

BIN
bench/bench Executable file

Binary file not shown.

View File

@@ -1,13 +1,17 @@
#define _POSIX_C_SOURCE 200809L
#include <errno.h>
#include <inttypes.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <time.h>
#include "ipc.h"
#include <sys/socket.h>
#include <sys/un.h>
enum {
Kback = 0xf008,
Kret = 0xf00d,
Kesc = 0xf01b,
Mctrl = 1<<2,
};
typedef struct Key Key;
struct Key {
@@ -16,7 +20,7 @@ struct Key {
};
static Key *keys;
static size_t nkeys;
static int nkeys;
static int fd;
static void
@@ -29,19 +33,13 @@ die(char *msg)
static void
addkey(int k, int mod)
{
static size_t cap;
Key *p;
static int cap;
if(nkeys >= cap){
if(cap > SIZE_MAX / 2 / sizeof(Key)){
fprintf(stderr, "too many keys\n");
exit(1);
}
cap = cap ? cap * 2 : 256;
p = realloc(keys, cap * sizeof(Key));
if(!p)
keys = realloc(keys, cap * sizeof(Key));
if(!keys)
die("realloc");
keys = p;
}
keys[nkeys].k = k;
keys[nkeys].mod = mod;
@@ -83,55 +81,49 @@ loadkeys(char *file)
}
}
fclose(f);
if(nkeys == 0){
fprintf(stderr, "%s: no keys\n", file);
exit(1);
}
}
static uint64_t
iterations(char *s)
{
uintmax_t n;
char *end;
if(*s == '\0' || *s == '-')
goto Bad;
errno = 0;
n = strtoumax(s, &end, 10);
if(errno != 0 || *end != '\0' || n == 0)
goto Bad;
return n;
Bad:
fprintf(stderr, "invalid iteration count: %s\n", s);
exit(1);
}
static void
dial(void)
{
fd = ipcconnect();
struct sockaddr_un addr;
fd = socket(AF_UNIX, SOCK_STREAM, 0);
if(fd < 0)
die("socket");
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
snprintf(addr.sun_path, sizeof(addr.sun_path),
"/tmp/strans.%d", getuid());
if(connect(fd, (struct sockaddr*)&addr, sizeof(addr)) < 0)
die("connect");
}
static void
sendkey(int key, int mod)
{
unsigned char req[Ipcreqsz];
unsigned char req[4];
ipcpackreq(req, 0, mod, key);
if(ipcsend(fd, req, sizeof req) < 0)
req[0] = 0;
req[1] = mod;
req[2] = key & 0xff;
req[3] = (key >> 8) & 0xff;
if(write(fd, req, 4) != 4)
die("write");
}
static int
readresp(void)
{
char commit[Ipcfieldmax+1], preedit[Ipcfieldmax+1];
Ipcresp resp;
unsigned char buf[256];
int n;
return ipcreadresp(fd, 0, commit, preedit, &resp);
if(read(fd, buf, 2) != 2)
return -1;
n = buf[1];
if(n > 0 && read(fd, buf + 2, n) != n)
return -1;
return 0;
}
static double
@@ -146,38 +138,31 @@ now(void)
int
main(int argc, char **argv)
{
size_t j;
uint64_t i, niter, total;
int i, j, niter;
double t0, t1, dt;
if(argc != 2 && argc != 3){
if(argc < 2){
fprintf(stderr, "usage: bench file [niter]\n");
exit(1);
}
loadkeys(argv[1]);
niter = argc == 3 ? iterations(argv[2]) : 1000;
if(niter > UINT64_MAX / nkeys){
fprintf(stderr, "iteration count overflows key total\n");
exit(1);
}
total = niter * nkeys;
niter = argc > 2 ? atoi(argv[2]) : 1000;
dial();
t0 = now();
for(i = 0; i < niter; i++)
for(j = 0; j < nkeys; j++){
sendkey(keys[j].k, keys[j].mod);
if(readresp() < 0){
fprintf(stderr, "failed at iter %" PRIu64 " key %zu\n", i, j);
fprintf(stderr, "failed at iter %d key %d\n", i, j);
exit(1);
}
}
t1 = now();
dt = t1 - t0;
printf("%" PRIu64 " iters x %zu keys = %" PRIu64 " keys\n",
niter, nkeys, total);
printf("%d iters x %d keys = %d keys\n", niter, nkeys, niter * nkeys);
printf("%.3f ms total, %.3f us/key, %.3f us/iter\n",
dt * 1000, dt * 1e6 / total, dt * 1e6 / niter);
dt * 1000, dt * 1e6 / (niter * nkeys), dt * 1e6 / niter);
close(fd);
return 0;
}

BIN
bench/perf.data Normal file

Binary file not shown.

BIN
bench/perf.data.old Normal file

Binary file not shown.

View File

@@ -1,82 +0,0 @@
#include "dat.h"
#include "fn.h"
#include <locale.h>
#include <xkbcommon/xkbcommon.h>
#include <xkbcommon/xkbcommon-compose.h>
/*
* One state per frontend, all made before the procs are: xkbcommon locks
* nothing, and even its table refcount is a plain increment. cowner is
* the context whose sequence a state holds.
*/
static struct xkb_compose_state *cstate[Ncompose];
static void *cowner[Ncompose];
/* Dead keys and Compose: no frontend's client does them for us. */
void
composeinit(void)
{
struct xkb_context *ctx;
struct xkb_compose_table *table;
char *locale;
int i;
locale = setlocale(LC_CTYPE, "");
ctx = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
if(ctx == nil)
return;
table = xkb_compose_table_new_from_locale(ctx,
locale != nil ? locale : "C", XKB_COMPOSE_COMPILE_NO_FLAGS);
if(table != nil)
for(i = 0; i < Ncompose; i++)
cstate[i] = xkb_compose_state_new(table,
XKB_COMPOSE_STATE_NO_FLAGS);
xkb_compose_table_unref(table);
xkb_context_unref(ctx);
}
/* The context is going, and the sequence it left half typed with it. */
void
composedrop(int who, void *owner)
{
if(cstate[who] == nil || owner != cowner[who])
return;
xkb_compose_state_reset(cstate[who]);
cowner[who] = nil;
}
/*
* Feeds sym to the Compose table. Returns 1 while a sequence is
* unfinished: the key belongs to the sequence, not to the engine. A
* finished sequence leaves its text in buf, which the caller commits
* after whatever the engine had pending. A sequence belongs to the
* context that started it: a key from another one starts over.
*/
int
composekey(int who, void *owner, u32int sym, char *buf, int n)
{
struct xkb_compose_state *cs;
buf[0] = '\0';
cs = cstate[who];
if(cs == nil)
return 0;
if(owner != cowner[who]){
xkb_compose_state_reset(cs);
cowner[who] = owner;
}
if(xkb_compose_state_feed(cs, sym) != XKB_COMPOSE_FEED_ACCEPTED)
return 0;
switch(xkb_compose_state_get_status(cs)){
case XKB_COMPOSE_COMPOSING:
case XKB_COMPOSE_CANCELLED:
return 1;
case XKB_COMPOSE_COMPOSED:
xkb_compose_state_get_utf8(cs, buf, n);
break;
default:
break;
}
return 0;
}

1
cutest

Submodule cutest deleted from 7894ba7a26

182
dat.h
View File

@@ -7,113 +7,87 @@
#define min(a, b) ((a) < (b) ? (a) : (b))
#define max(a, b) ((a) > (b) ? (a) : (b))
/*
* plan9port runs the note handlers for a note it goes on to ignore, and
* the process lives through it, so what strans announced must outlive
* such a note. A daemon that forks nothing gets only the one.
*/
#define notefatal(note) (strcmp(note, "sys: write on closed pipe") != 0)
/* A language id is the control code of the Ctrl key that selects it. */
enum
{
LangEN = 0x14,
LangJP = 0x0e,
LangJPK = 0x0b,
LangKO = 0x13,
LangHANJA = 0x08,
LangEMOJI = 0x05,
LangVI = 0x16,
Maxrunes = 64,
Maxutf = Maxrunes * UTFmax + 1,
Fontsz = 32,
Fontbase = 4,
Nglyphs = 0x20000,
};
enum
{
Maxclients = 64,
Ownerpoll = 200, /* ms between a frontend's owner checks while its preedit shows */
Maxkouho = 128,
Maxdisp = 9,
/* Content purposes whose keys the engine never sees; IBus and
* text-input-v3 number them alike, both after GTK. */
Purposepassword = 8,
Purposepin = 9,
Maxkouho = 32,
Maxdisp = 8,
Imgw = 512,
Imgh = Maxdisp * Fontsz + 8,
Colfg = 0x000000,
Colbg = 0xffffff,
Colsep = 0xd0d0d0,
Colsel = 0x333333,
Colselfg = 0xffffff,
Colsel = 0xcccccc,
};
/* Each frontend composes on its own: they run in different procs. */
enum
{
Composeibus,
Composexim,
Composewl,
Ncompose,
};
/* Popup metrics in pixels; popupscale is GDK_SCALE, for HiDPI. */
extern int popupscale;
#define Fontsz (32*popupscale)
#define PopupPad (4*popupscale)
#define PopupSep popupscale
#define PopupBorder popupscale
#define PopupNumw Fontsz
#define PopupTextw (12*Fontsz)
#define PopupBasew (2*PopupPad + PopupNumw + PopupTextw)
typedef struct Str Str;
struct Str
{
Rune r[Maxrunes];
Rune r[64];
int n;
};
/*
* What one key does to a composition: text to commit, the new pending
* text, and the new raw state (Telex key history, or pending Japanese
* romaji), given the old ones in Im.
*/
typedef struct Emit Emit;
struct Emit
{
int eat;
Str s;
Str next;
Str raw;
Str dict;
};
typedef struct Hnode Hnode;
struct Hnode
{
int filled;
int next;
char *key;
int klen;
char *val;
int vlen;
};
typedef struct Tnode Tnode;
struct Tnode
{
char *val;
int vlen;
int child;
int sibling;
Rune c;
char c;
char *val;
int vlen;
};
typedef struct Trie Trie;
struct Trie
{
int root;
Tnode *nodes;
int n;
int cap;
/* the last key put and its path, so a sorted file walks it once */
Str last;
int path[Maxrunes];
};
enum
typedef struct Hmap Hmap;
struct Hmap
{
TrieMiss,
TriePrefix,
TrieExact,
int nbs;
int nsz;
int len;
int cap;
uchar *nodes;
};
typedef struct Lang Lang;
@@ -125,106 +99,56 @@ struct Lang
char *dictname;
Emit (*trans)(Im*, Rune);
void (*back)(Im*);
void (*dictq)(Im*);
Trie *map;
Trie *dict;
Hmap *dict;
};
struct Im
{
Lang *l;
Str pre;
/* Telex history, or the short pending romaji in a Japanese mode. */
Str raw;
Str kouho[Maxkouho];
int nkouho;
int sel;
};
typedef struct Drawcmd Drawcmd;
typedef struct Caret Caret;
typedef struct Area Area;
struct Area
{
int x;
int y;
int w;
int h;
};
struct Caret
{
int valid;
int x;
int y;
int h;
};
struct Drawcmd
{
Str pre;
Str kouho[Maxdisp];
int nkouho;
int sel;
int first;
int total;
Caret caret;
};
/*
* Popup geometry from popuplayout: n rows from row0 of the candidates,
* each Fontsz tall; a section's y is -1 when it is absent. Numbers
* start at PopupPad, text at PopupPad+PopupNumw.
*/
typedef struct Popup Popup;
struct Popup
{
int w;
int h;
int n;
int row0;
int prey;
int sepy;
int rowsy;
int textw;
int marky;
int markx;
int markw;
Str mark;
int sely;
int selw;
};
typedef struct Keyreq Keyreq;
typedef struct Keyres Keyres;
enum
{
Keypress,
Keyreset,
Keyrelease,
Keycaret,
Keycap,
};
struct Keyres
{
int eaten; /* Keycap: the caller is the owner and clientpre took. */
int del; /* runes of the client's own text to take back first */
Str commit;
Str preedit;
};
struct Keyreq
{
void *owner; /* stable until the context's release is acknowledged */
int clientpre; /* the client draws the preedit; the popup does not */
int op;
int fd;
u32int ks;
u32int mod;
Caret caret;
Str surround; /* the client's own text just before the cursor */
Channel *reply;
};
typedef struct Dictreq Dictreq;
struct Dictreq
{
Str key;
Str pre;
int lang;
};
typedef struct Dictres Dictres;
struct Dictres
{
Str key;
Str kouho[Maxkouho];
int nkouho;
};
extern Lang langs[];
extern int nlang;
extern Channel *drawc;
extern Channel *keyc;
extern Channel *dictreqc;
extern Channel *dictresc;

156
dict.c
View File

@@ -1,90 +1,102 @@
#include "dat.h"
#include "fn.h"
/* Appends the space-separated words of a node's entry to out[], up to max. */
static int
words(Tnode *nd, Str *out, int n, int max)
static void
dictlookup(Dictreq *req, Dictres *res)
{
Lang *l;
Hmap *dict;
Hnode *n;
char *p, *e, *sp;
Str tmp;
if(nd->val == nil)
return n;
p = nd->val;
e = p + nd->vlen;
while(n < max && p < e){
while(p < e && *p == ' ')
p++;
res->nkouho = 0;
if(req->key.n == 0)
return;
l = getlang(req->lang);
dict = l ? l->dict : nil;
if(dict == nil)
return;
n = hmapget(dict, &req->key);
if(n == nil || n->vlen == 0)
return;
p = n->val;
e = p + n->vlen;
while(res->nkouho < Maxkouho && p < e){
sp = p;
while(p < e && *p != ' ')
p++;
if(sinit(&tmp, sp, p - sp) && tmp.n > 0)
out[n++] = tmp;
sinit(&tmp, sp, p - sp);
if(scmp(&tmp, &req->key) != 0)
res->kouho[res->nkouho++] = tmp;
if(p < e)
p++;
}
return n;
}
/* The entries at and below a node, the shortest keys first. */
static int
below(Trie *t, int ni, Str *out, int n, int max)
{
Tnode *nd;
int ci;
nd = &t->nodes[ni];
n = words(nd, out, n, max);
for(ci = nd->child; ci >= 0 && n < max; ci = t->nodes[ci].sibling)
n = below(t, ci, out, n, max);
return n;
}
/* Fills out[] with up to max candidates for key: the words of its entry. */
int
dictlookup(Trie *t, Str *key, Str *out, int max)
{
int ni;
ni = key->n == 0 ? -1 : trienode(t, key);
if(ni < 0)
return 0;
return words(&t->nodes[ni], out, 0, max);
}
/* As dictlookup, for every entry key is a prefix of, key's own first. */
int
dictprefix(Trie *t, Str *key, Str *out, int max)
{
int ni;
ni = key->n == 0 ? -1 : trienode(t, key);
if(ni < 0)
return 0;
return below(t, ni, out, 0, max);
}
static Trie*
langopen(char *dir, char *name, char *ext)
{
char *path;
Trie *t;
path = smprint("%s/%s.%s", dir, name, ext);
if(path == nil)
die("out of memory");
t = trieopen(path);
free(path);
return t;
}
void
langinit(char *dir)
dictthread(void*)
{
Lang *l;
Dictreq req;
Dictres res;
for(l = langs; l < langs + nlang; l++){
if(l->mapname != nil)
l->map = langopen(dir, l->mapname, "map");
if(l->dictname != nil)
l->dict = langopen(dir, l->dictname, "dict");
threadsetname("dict");
for(;;){
if(chanrecv(dictreqc, &req) < 0)
break;
while(channbrecv(dictreqc, &req) > 0)
;
dictlookup(&req, &res);
res.key = req.pre;
chansend(dictresc, &res);
}
}
static Hmap*
dictopen(char *path)
{
Hmap *h;
Biobuf *b;
Str key, val;
char *line, *tab;
int len;
b = Bopen(path, OREAD);
if(b == nil)
die("can't open: %s", path);
h = hmapalloc(4096, 0);
while((line = Brdstr(b, '\n', 1)) != nil){
len = strlen(line);
if(len == 0 || line[0] == ';'){
free(line);
continue;
}
tab = strchr(line, '\t');
if(tab == nil || tab >= line + len - 1){
free(line);
continue;
}
*tab = '\0';
sinit(&key, line, tab - line);
sinit(&val, tab+1, len - (tab - line) - 1);
hmapset(&h, &key, &val);
free(line);
}
Bterm(b);
return h;
}
void
dictinit(char *dir)
{
char path[1024];
int i;
for(i = 0; i < nlang; i++){
if(langs[i].dictname == nil)
continue;
snprint(path, sizeof(path), "%s/%s.dict", dir, langs[i].dictname);
langs[i].dict = dictopen(path);
}
}

44
fn.h
View File

@@ -1,7 +1,6 @@
void die(char*, ...);
int sinit(Str*, char*, int);
void stail(Str*, char*, int);
void sinit(Str*, char*, int);
void sclear(Str*);
void sputr(Str*, Rune);
void spopr(Str*);
@@ -10,52 +9,33 @@ int scmp(Str*, Str*);
int stoutf(Str*, char*, int);
Rune slastr(Str*);
Hmap* hmapalloc(int, int);
int hmapset(Hmap**, Str*, Str*);
Hnode* hmapget(Hmap*, Str*);
int mapget(Trie*, Str*, Str*);
Trie* trienew(void);
void trieput(Trie*, char*, int, char*, int);
Trie* trieopen(char*);
void trieclose(Trie*);
int trienode(Trie*, Str*);
int trielookup(Trie*, Str*, char**, int*);
char* trieget(Trie*, char*, int, int*);
int trielookup(Trie*, char*, int, char**, int*);
Lang* getlang(int);
void langinit(char*);
void mapinit(char*);
void dictinit(char*);
int dictlookup(Trie*, Str*, Str*, int);
int dictprefix(Trie*, Str*, Str*, int);
void dictthread(void*);
void drawthread(void*);
void popuparea(Area*, int, Area*, int, int, Area*);
void popupposition(Caret*, int, int, Area*, int, int, int*, int*);
int pagemarker(char*, int, int, int, int);
void popuplayout(Drawcmd*, int, int, Popup*);
void popupdraw(u32int*, Drawcmd*, Popup*);
void imthread(void*);
void impre(Im*, Str*);
void transstr(Lang*, Str*, Str*, Str*);
Emit transmap(Im*, Rune);
Emit transko(Im*, Rune);
Emit transvi(Im*, Rune);
void backko(Im*);
void backvi(Im*);
void dictsend(Im*, Str*);
void srvinit(void);
void srvthread(void*);
void ibusthread(void*);
void ximthread(void*);
int wlinit(void);
void wlthread(void*);
int keymeaningful(u32int);
void composeinit(void);
void composedrop(int, void*);
int composekey(int, void*, u32int, char*, int);
void* emalloc(ulong);
void* erealloc(void*, ulong);
void textinit(void);
void textclose(void);
int textwidth(Str*);
void textdraw(u32int*, int, int, int, int, int, u32int, Str*);
void fontinit(char*);
void putfont(u32int*, int, int, int, int, Rune);

233
font.c
View File

@@ -1,142 +1,137 @@
#define STB_TRUETYPE_IMPLEMENTATION
#include "stb_truetype.h"
#include "dat.h"
#include <fontconfig/fontconfig.h>
#include <pango/pangocairo.h>
#include "fn.h"
static PangoFontMap *fontmap;
static PangoContext *context;
static PangoLayout *layout;
void
textclose(void)
typedef struct Glyph Glyph;
struct Glyph
{
if(layout != nil)
g_object_unref(layout);
if(context != nil)
g_object_unref(context);
if(fontmap != nil)
g_object_unref(fontmap);
layout = nil;
context = nil;
fontmap = nil;
uchar *bmp;
int w, h, ox, oy;
};
enum { Maxfonts = 4 };
static stbtt_fontinfo fonts[Maxfonts];
static uchar *fontdata[Maxfonts];
static float scale[Maxfonts];
static int nfonts;
static Glyph cache[Nglyphs];
static u32int blendtab[2][256];
static u32int
blend(u32int bg, int a)
{
int r, g, b, inv;
inv = 255 - a;
r = (bg >> 16 & 0xff) * inv / 255;
g = (bg >> 8 & 0xff) * inv / 255;
b = (bg & 0xff) * inv / 255;
return (r << 16) | (g << 8) | b;
}
static void
setfont(void)
loadfont(char *path)
{
PangoFontDescription *font;
PangoRectangle r;
int size;
int fd;
long sz, n;
font = pango_font_description_new();
pango_font_description_set_family(font, "sans");
pango_font_description_set_absolute_size(font, Fontsz * PANGO_SCALE);
pango_layout_set_font_description(layout, font);
pango_layout_set_text(layout, "Mg", -1);
pango_layout_get_pixel_extents(layout, nil, &r);
if(r.height > 0){
/* Fontsz is the popup row height, not a point size. */
size = Fontsz * PANGO_SCALE * Fontsz / r.height;
pango_font_description_set_absolute_size(font, max(size, PANGO_SCALE));
pango_layout_set_font_description(layout, font);
}
pango_font_description_free(font);
}
void
textinit(void)
{
FcInit();
fontmap = pango_cairo_font_map_new();
pango_cairo_font_map_set_resolution(PANGO_CAIRO_FONT_MAP(fontmap), 96);
context = pango_font_map_create_context(fontmap);
layout = pango_layout_new(context);
pango_layout_set_single_paragraph_mode(layout, TRUE);
setfont();
if(nfonts >= Maxfonts)
die("too many fonts");
fd = open(path, OREAD);
if(fd < 0)
die("can't open font: %s", path);
sz = seek(fd, 0, 2);
seek(fd, 0, 0);
fontdata[nfonts] = emalloc(sz);
n = readn(fd, fontdata[nfonts], sz);
close(fd);
if(n != sz)
die("can't read font: %s", path);
if(!stbtt_InitFont(&fonts[nfonts], fontdata[nfonts], stbtt_GetFontOffsetForIndex(fontdata[nfonts], 0)))
die("can't init font: %s", path);
scale[nfonts] = stbtt_ScaleForPixelHeight(&fonts[nfonts], Fontsz);
nfonts++;
}
static int
settext(Str *s)
isfont(char *name)
{
char utf[Maxutf];
int n;
char *p;
n = stoutf(s, utf, sizeof utf);
pango_layout_set_text(layout, utf, n);
return n > 0;
}
static void
textextents(PangoRectangle *r)
{
PangoRectangle ink, logical;
int x0, y0, x1, y1;
pango_layout_get_pixel_extents(layout, &ink, &logical);
x0 = min(ink.x, logical.x);
y0 = min(ink.y, logical.y);
x1 = max(ink.x + ink.width, logical.x + logical.width);
y1 = max(ink.y + ink.height, logical.y + logical.height);
r->x = x0;
r->y = y0;
r->width = max(x1 - x0, 0);
r->height = max(y1 - y0, 0);
}
int
textwidth(Str *s)
{
PangoRectangle r;
pango_layout_set_width(layout, -1);
pango_layout_set_ellipsize(layout, PANGO_ELLIPSIZE_NONE);
if(!settext(s))
p = strrchr(name, '.');
if(p == nil)
return 0;
textextents(&r);
return r.width;
return strcmp(p, ".ttf") == 0 || strcmp(p, ".otf") == 0;
}
/*
* Draws s at (x, y) into the w-by-h buffer, ellipsized to fit pixels and
* clipped to its own row so a tall fallback face cannot paint a
* neighbour.
*/
void
textdraw(u32int *buf, int w, int h, int x, int y, int fit, u32int color,
Str *s)
fontinit(char *dir)
{
PangoRectangle r;
cairo_surface_t *surface;
cairo_t *cr;
int b, g, red;
int fd, n, i, a;
Dir *d;
char path[256];
if(fit <= 0)
return;
pango_layout_set_width(layout, fit * PANGO_SCALE);
pango_layout_set_ellipsize(layout, PANGO_ELLIPSIZE_END);
if(w <= 0 || h <= 0 || !settext(s))
return;
surface = cairo_image_surface_create_for_data((uchar*)buf,
CAIRO_FORMAT_RGB24, w, h, w * sizeof buf[0]);
if(cairo_surface_status(surface) != CAIRO_STATUS_SUCCESS){
cairo_surface_destroy(surface);
return;
fd = open(dir, OREAD);
if(fd < 0)
die("can't open font dir: %s", dir);
n = dirreadall(fd, &d);
close(fd);
if(n < 0)
die("can't read font dir: %s", dir);
for(i = 0; i < n; i++){
if(isfont(d[i].name)){
snprint(path, sizeof path, "%s/%s", dir, d[i].name);
loadfont(path);
}
}
cairo_surface_mark_dirty(surface);
cr = cairo_create(surface);
if(cairo_status(cr) == CAIRO_STATUS_SUCCESS){
pango_cairo_update_layout(cr, layout);
textextents(&r);
cairo_rectangle(cr, x, y, fit, Fontsz);
cairo_clip(cr);
cairo_move_to(cr, x - r.x, y + (Fontsz - r.height) / 2 - r.y);
red = color >> 16 & 0xff;
g = color >> 8 & 0xff;
b = color & 0xff;
cairo_set_source_rgb(cr, red / 255.0, g / 255.0, b / 255.0);
pango_cairo_show_layout(cr, layout);
free(d);
if(nfonts == 0)
die("no fonts in %s", dir);
for(a = 0; a < 256; a++){
blendtab[0][a] = blend(Colbg, a);
blendtab[1][a] = blend(Colsel, a);
}
}
void
putfont(u32int *buf, int w, int h, int px, int py, Rune r)
{
Glyph *g;
int i, j, a, sel, f;
int y0, j0, j1, x0, i0, i1;
u32int *p;
if(r >= Nglyphs)
return;
g = &cache[r];
if(g->bmp == nil){
for(f = 0; f < nfonts; f++){
if(stbtt_FindGlyphIndex(&fonts[f], r) == 0)
continue;
g->bmp = stbtt_GetCodepointBitmap(&fonts[f], scale[f], scale[f], r, &g->w, &g->h, &g->ox, &g->oy);
if(g->bmp != nil)
break;
}
if(g->bmp == nil)
return;
}
y0 = py + g->oy + Fontsz - Fontbase;
j0 = y0 < 0 ? -y0 : 0;
j1 = y0 + g->h > h ? h - y0 : g->h;
x0 = px + g->ox;
i0 = x0 < 0 ? -x0 : 0;
i1 = x0 + g->w > w ? w - x0 : g->w;
for(j = j0; j < j1; j++){
for(i = i0; i < i1; i++){
a = g->bmp[j * g->w + i];
if(a > 0){
p = &buf[(y0 + j) * w + x0 + i];
sel = (*p == Colsel) ? 1 : 0;
*p = blendtab[sel][a];
}
}
}
cairo_destroy(cr);
cairo_surface_flush(surface);
cairo_surface_destroy(surface);
}

BIN
font/NotoEmoji-Regular.ttf Normal file

Binary file not shown.

BIN
font/NotoSans-Regular.ttf Normal file

Binary file not shown.

Binary file not shown.

3
gtk/Dockerfile Normal file
View File

@@ -0,0 +1,3 @@
FROM debian:stable-slim
RUN apt-get update && apt-get install -y gcc libgtk-3-dev make
WORKDIR /src

View File

@@ -1,43 +1,22 @@
PROG = im-strans.so
PKG_CONFIG ?= pkg-config
CFLAGS ?= -O2
GTK_CFLAGS = $(shell $(PKG_CONFIG) --cflags gtk+-3.0)
GTK_LIBS = $(shell $(PKG_CONFIG) --libs gtk+-3.0)
GTK_QUERY_IMMODULES ?= gtk-query-immodules-3.0
GTK_LIBDIR ?= $(shell $(PKG_CONFIG) --variable=libdir gtk+-3.0 2>/dev/null)
GTK_BINARY_VERSION ?= $(shell $(PKG_CONFIG) \
--variable=gtk_binary_version gtk+-3.0 2>/dev/null)
# Without GTK development files, ask the runtime where its modules live.
GTK_RUNTIME_MODULE_DIR = $(shell GTK_PATH= GTK_IM_MODULE_FILE= \
$(GTK_QUERY_IMMODULES) 2>/dev/null | \
awk -F '"' '/^"\// { sub("/[^/]*$$", "", $$2); print $$2; exit }')
GTK_MODULE_DIR ?= $(strip $(if \
$(and $(strip $(GTK_LIBDIR)),$(strip $(GTK_BINARY_VERSION))),\
$(GTK_LIBDIR)/gtk-3.0/$(GTK_BINARY_VERSION)/immodules,\
$(GTK_RUNTIME_MODULE_DIR)))
CFLAGS = -Wall -O2 -I.. `pkg-config --cflags gtk+-3.0`
LDFLAGS = `pkg-config --libs gtk+-3.0`
DSTDIR ?= $(shell find /usr/lib* -type d -path "*/gtk-3.0/*/immodules" 2>/dev/null | head -1)
all: $(PROG)
im-strans.so: main.c
$(CC) -shared -fPIC $(CFLAGS) -o $@ $< $(LDFLAGS)
$(PROG): main.c ../ipc.c ../ipc.h
$(CC) $(CPPFLAGS) -I.. $(GTK_CFLAGS) -Wall $(CFLAGS) $(LDFLAGS) \
-shared -fPIC -o $@ main.c ../ipc.c $(GTK_LIBS) $(LDLIBS)
docker: main.c Dockerfile
docker build -t strans-gtk .
docker run --rm -v $(CURDIR)/..:/src -w /src/gtk strans-gtk make im-strans.so
CHECKDIR = test -n "$$module_dir" || { \
echo "cannot find GTK 3 module directory; set GTK_MODULE_DIR" >&2; \
exit 1; }
REFRESH = test -n "$(DESTDIR)" || { \
unset GTK_PATH GTK_IM_MODULE_FILE; $(GTK_QUERY_IMMODULES) --update-cache; }
install: $(PROG)
module_dir="$(GTK_MODULE_DIR)"; $(CHECKDIR); \
mkdir -p "$(DESTDIR)$$module_dir" && \
cp "$(PROG)" "$(DESTDIR)$$module_dir/" && $(REFRESH)
install:
mkdir -p $(DSTDIR)
cp im-strans.so $(DSTDIR)/
gtk-query-immodules-3.0 --update-cache
uninstall:
module_dir="$(GTK_MODULE_DIR)"; $(CHECKDIR); \
rm -f "$(DESTDIR)$$module_dir/$(PROG)" && $(REFRESH)
rm -f $(DSTDIR)/im-strans.so
gtk-query-immodules-3.0 --update-cache
clean:
rm -f $(PROG)
.PHONY: all install uninstall clean
rm -f im-strans.so

View File

@@ -1,545 +1,165 @@
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <gtk/gtk.h>
#ifdef GDK_WINDOWING_X11
#include <gdk/gdkx.h>
#endif
#include "ipc.h"
typedef struct Im Im;
struct Im
{
GtkIMContextSimple parent;
GtkIMContext parent;
int fd;
int usepreedit;
int ext;
int private;
int simpleactive;
int simpledone;
char pre[Ipcfieldmax+1];
int prelen;
GdkWindow *win;
GdkRectangle cursor;
int cursorvalid;
int caretsent;
unsigned char sent[Ipccaretsz];
char surround[Ipcfieldmax];
int nsurround;
int surroundsent;
};
typedef struct ImClass ImClass;
struct ImClass
{
GtkIMContextSimpleClass parent;
GtkIMContextClass parent;
};
static GType imtype;
static GObjectClass *parentobject;
static GtkIMContextClass *parentim;
static void
setpreedit(Im *im, const char *s, int n)
{
int was;
if(n < 0 || n > Ipcfieldmax)
n = 0;
if(n == 0 && im->prelen == 0)
return;
was = im->prelen;
if(n > 0)
memcpy(im->pre, s, n);
im->pre[n] = '\0';
im->prelen = n;
if(was == 0 && n > 0)
g_signal_emit_by_name(im, "preedit-start");
g_signal_emit_by_name(im, "preedit-changed");
if(was > 0 && n == 0)
g_signal_emit_by_name(im, "preedit-end");
}
static void
srvdrop(Im *im, int notify)
{
if(im->fd >= 0)
close(im->fd);
im->fd = -1;
im->ext = 0;
im->caretsent = 0;
im->surroundsent = 0;
if(notify)
setpreedit(im, "", 0);
else{
im->pre[0] = '\0';
im->prelen = 0;
}
}
static void
srvclose(Im *im)
{
srvdrop(im, 1);
}
/* Both buffers are char[Ipcfieldmax+1]. */
static int
readresp(Im *im, int want, char *commit, char *pre, Ipcresp *resp)
{
if(ipcreadresp(im->fd, want, commit, pre, resp) < 0)
return -1;
if(!g_utf8_validate(commit, resp->commitlen, NULL) ||
(want && !g_utf8_validate(pre, resp->preeditlen, NULL))){
errno = EPROTO;
return -1;
}
return 0;
}
static void
dropwindow(Im *im)
{
if(im->win != NULL)
g_object_unref(im->win);
im->win = NULL;
im->cursorvalid = 0;
}
/* Root-relative caret in device pixels; returns whether it is known. */
static int
caretget(Im *im, int32_t *x, int32_t *y, int32_t *h)
{
gint rx, ry, scale;
int64_t sx, sy, sh;
*x = *y = *h = 0;
if(im->win != NULL && gdk_window_is_destroyed(im->win))
dropwindow(im);
if(!im->cursorvalid || im->win == NULL || im->cursor.height < 0)
return 0;
#ifdef GDK_WINDOWING_X11
if(!GDK_IS_X11_WINDOW(im->win))
return 0;
#else
return 0;
#endif
scale = gdk_window_get_scale_factor(im->win);
if(scale <= 0)
return 0;
/* GDK translates the client-relative point before device scaling. */
gdk_window_get_root_coords(im->win, 0, 0, &rx, &ry);
sx = ((int64_t)rx + im->cursor.x) * scale;
sy = ((int64_t)ry + im->cursor.y) * scale;
sh = (int64_t)im->cursor.height * scale;
if(sx < INT32_MIN || sx > INT32_MAX ||
sy < INT32_MIN || sy > INT32_MAX || sh > INT32_MAX)
return 0;
*x = sx;
*y = sy;
*h = sh;
return 1;
}
static int
sendcaret(Im *im)
{
unsigned char buf[Ipccaretsz];
int32_t x, y, h;
int valid;
if(im->fd < 0 || !im->ext)
return 0;
valid = caretget(im, &x, &y, &h);
ipcpackcaret(buf, valid, x, y, h);
if(im->caretsent && memcmp(buf, im->sent, sizeof buf) == 0)
return 0;
if(ipcsend(im->fd, buf, sizeof buf) < 0)
return -1;
memcpy(im->sent, buf, sizeof buf);
im->caretsent = 1;
return 0;
}
/*
* The text just before the cursor, as much of it as a reading can use,
* cut on a rune boundary. A widget that keeps none leaves it empty, and
* then the daemon never asks for any of it back.
*/
static void
readsurround(Im *im, GtkIMContext *ctx)
{
char *text, *p;
gint cursor;
int start;
im->nsurround = 0;
if(!gtk_im_context_get_surrounding(ctx, &text, &cursor))
return;
if(cursor < 0 || cursor > (gint)strlen(text)){
g_free(text);
return;
}
start = cursor > Ipcfieldmax ? cursor - Ipcfieldmax : 0;
for(p = text + start; p < text + cursor && (*p & 0xc0) == 0x80; p++)
;
im->nsurround = text + cursor - p;
memcpy(im->surround, p, im->nsurround);
g_free(text);
}
/* Sent only when it changes, as the caret is. */
static int
sendsurround(Im *im, GtkIMContext *ctx)
{
unsigned char hdr[Ipcreqsz];
char was[Ipcfieldmax];
int nwas;
if(im->fd < 0 || !im->ext)
return 0;
nwas = im->nsurround;
memcpy(was, im->surround, nwas);
readsurround(im, ctx);
if(im->surroundsent && nwas == im->nsurround &&
memcmp(was, im->surround, nwas) == 0)
return 0;
ipcpacksurround(hdr, im->nsurround);
if(ipcsend(im->fd, hdr, sizeof hdr) < 0 ||
(im->nsurround > 0 &&
ipcsend(im->fd, im->surround, im->nsurround) < 0))
return -1;
im->surroundsent = 1;
return 0;
}
static int
srvconnect(Im *im)
{
unsigned char buf[Ipcreqsz];
char commit[Ipcfieldmax+1];
char pre[Ipcfieldmax+1];
Ipcresp resp;
struct sockaddr_un addr;
if(im->fd >= 0)
return 0;
im->fd = ipcconnect();
return;
im->fd = socket(AF_UNIX, SOCK_STREAM, 0);
if(im->fd < 0)
return -1;
/* Marker zero is an old daemon's harmless key-zero response. */
ipcpackcap(buf, im->usepreedit);
if(ipcsend(im->fd, buf, sizeof buf) < 0 ||
readresp(im, im->usepreedit, commit, pre, &resp) < 0){
srvclose(im);
return -1;
return;
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
snprintf(addr.sun_path, sizeof(addr.sun_path), "/tmp/strans.%d", getuid());
if(connect(im->fd, (struct sockaddr*)&addr, sizeof(addr)) < 0){
close(im->fd);
im->fd = -1;
}
im->ext = resp.eaten != 0;
if(sendcaret(im) < 0){
srvclose(im);
}
static int
readresp(Im *im, char *buf, int bufsz)
{
unsigned char hdr[2];
int n;
if(read(im->fd, hdr, 2) != 2)
return -1;
n = hdr[1];
buf[0] = '\0';
if(n > 0 && n < bufsz){
if(read(im->fd, buf, n) != n)
return -1;
buf[n] = '\0';
}
return 0;
return hdr[0];
}
static void
simplecommit(GtkIMContext *ctx, const char *s, Im *im)
static uint32_t
kget(uint32_t gdk)
{
(void)ctx;
(void)s;
im->simpledone = 1;
if(gdk >= 0xff00)
return Kspec + (gdk - 0xff00);
return gdk_keyval_to_unicode(gdk);
}
static void
simpleend(GtkIMContext *ctx, Im *im)
static uint32_t
mget(uint32_t state)
{
(void)ctx;
im->simpledone = 1;
uint32_t m;
m = 0;
if(state & GDK_SHIFT_MASK)
m |= Mshift;
if(state & GDK_CONTROL_MASK)
m |= Mctrl;
if(state & GDK_MOD1_MASK)
m |= Malt;
if(state & GDK_SUPER_MASK)
m |= Msuper;
return m;
}
/*
* GtkIMContextSimple composes dead keys and Compose sequences; it keeps
* the key stream until it commits, ends its preedit, or rejects a press.
*/
static gboolean
simplefilter(GtkIMContext *ctx, GdkEventKey *ev, int release)
{
Im *im;
gboolean r;
im = (Im*)ctx;
im->simpleactive = 1;
im->simpledone = 0;
r = parentim->filter_keypress(ctx, ev);
if((!r && !release) || im->simpledone)
im->simpleactive = 0;
return r;
}
/* The daemon hands the pending text back; it becomes committed text. */
static void
sendreset(Im *im)
{
unsigned char buf[Ipcreqsz];
char commit[Ipcfieldmax+1];
char pre[Ipcfieldmax+1];
Ipcresp resp;
unsigned char buf[4];
char resp[64];
if(im->fd < 0){
setpreedit(im, "", 0);
if(im->fd < 0)
return;
}
ipcpackreset(buf, im->usepreedit);
if(ipcsend(im->fd, buf, sizeof buf) < 0 ||
readresp(im, im->usepreedit, commit, pre, &resp) < 0){
srvclose(im);
return;
}
setpreedit(im, "", 0);
if(commit[0] != '\0')
g_signal_emit_by_name(im, "commit", commit);
}
/* A password entry hidden the older way sets no purpose and signals nothing. */
static int
ishidden(Im *im)
{
GtkWidget *w;
if(im->win == NULL)
return 0;
gdk_window_get_user_data(im->win, (gpointer*)&w);
return GTK_IS_ENTRY(w) && !gtk_entry_get_visibility(GTK_ENTRY(w));
buf[0] = 0;
buf[1] = 0;
buf[2] = Kesc & 0xff;
buf[3] = Kesc >> 8;
if(write(im->fd, buf, 4) == 4)
readresp(im, resp, sizeof(resp));
}
static gboolean
kpress(GtkIMContext *ctx, GdkEventKey *ev)
{
Im *im;
unsigned char buf[Ipcreqsz];
char commit[Ipcfieldmax+1];
char pre[Ipcfieldmax+1];
unsigned char buf[4];
char resp[64];
uint32_t key, mod;
Ipcresp resp;
gboolean r;
int r;
im = (Im*)ctx;
if(ev->type != GDK_KEY_PRESS){
if(im->simpleactive)
return simplefilter(ctx, ev, 1);
if(ev->type != GDK_KEY_PRESS)
return FALSE;
}
if(im->simpleactive)
return simplefilter(ctx, ev, 0);
key = ipckeysym(ev->keyval, gdk_keyval_to_unicode(ev->keyval));
mod = ipcmod(ev->state);
if(im->private || ishidden(im) || key == 0){
r = simplefilter(ctx, ev, 0);
/* A dead key starts a compose: pending text was typed first. */
if(im->simpleactive)
sendreset(im);
return r;
}
if(srvconnect(im) < 0)
return simplefilter(ctx, ev, 0);
/* A retained GDK window may have moved since the last cursor report. */
if(sendcaret(im) < 0 || sendsurround(im, ctx) < 0){
srvclose(im);
return simplefilter(ctx, ev, 0);
}
ipcpackreq(buf, im->usepreedit, mod, key);
if(ipcsend(im->fd, buf, sizeof buf) < 0 ||
readresp(im, im->usepreedit, commit, pre, &resp) < 0){
srvclose(im);
return simplefilter(ctx, ev, 0);
}
if(im->usepreedit && commit[0] != '\0' && im->prelen > 0)
setpreedit(im, "", 0);
/* The reading reached into the widget's own text: give it back. */
if(resp.del > 0){
gtk_im_context_delete_surrounding(ctx, -resp.del, resp.del);
im->surroundsent = 0;
}
if(commit[0] != '\0')
g_signal_emit_by_name(ctx, "commit", commit);
if(im->usepreedit)
setpreedit(im, pre, resp.preeditlen);
if(resp.eaten)
return TRUE;
return simplefilter(ctx, ev, 0);
}
static void
getpreedit(GtkIMContext *ctx, gchar **str, PangoAttrList **attrs,
gint *cursor_pos)
{
Im *im;
PangoAttribute *u;
im = (Im*)ctx;
if(im->simpleactive){
parentim->get_preedit_string(ctx, str, attrs, cursor_pos);
return;
}
if(str)
*str = g_strdup(im->pre);
if(attrs){
*attrs = pango_attr_list_new();
if(im->prelen > 0){
u = pango_attr_underline_new(PANGO_UNDERLINE_SINGLE);
u->start_index = 0;
u->end_index = im->prelen;
pango_attr_list_insert(*attrs, u);
}
}
if(cursor_pos)
*cursor_pos = g_utf8_strlen(im->pre, -1);
srvconnect(im);
if(im->fd < 0)
return FALSE;
key = kget(ev->keyval);
if(key == 0)
return FALSE;
mod = mget(ev->state);
buf[0] = 0;
buf[1] = mod;
buf[2] = key & 0xff;
buf[3] = key >> 8;
if(write(im->fd, buf, 4) != 4)
return FALSE;
r = readresp(im, resp, sizeof(resp));
if(r < 0)
return FALSE;
if(r != 0 && resp[0] != '\0')
g_signal_emit_by_name(ctx, "commit", resp);
return r != 0;
}
static void
reset(GtkIMContext *ctx)
{
if(parentim->reset != NULL)
parentim->reset(ctx);
((Im*)ctx)->simpleactive = 0;
sendreset((Im*)ctx);
}
static void
focusout(GtkIMContext *ctx)
{
Im *im;
im = (Im*)ctx;
if(parentim->focus_out != NULL)
parentim->focus_out(ctx);
im->simpleactive = 0;
/* Closing the connection releases the engine. */
sendreset(im);
srvclose(im);
sendreset((Im*)ctx);
}
static void
setusepreedit(GtkIMContext *ctx, gboolean use)
{
Im *im;
unsigned char buf[Ipcreqsz];
char commit[Ipcfieldmax+1];
char pre[Ipcfieldmax+1];
Ipcresp resp;
im = (Im*)ctx;
use = use != FALSE;
if(parentim->set_use_preedit != NULL)
parentim->set_use_preedit(ctx, use);
if(im->usepreedit == use)
return;
im->usepreedit = use;
if(!use)
setpreedit(im, "", 0);
if(im->fd < 0 || !im->ext)
return;
ipcpackcap(buf, use);
if(ipcsend(im->fd, buf, sizeof buf) < 0 ||
readresp(im, use, commit, pre, &resp) < 0 ||
!resp.eaten){
srvclose(im);
return;
}
if(use)
setpreedit(im, pre, resp.preeditlen);
}
static void
setclientwindow(GtkIMContext *ctx, GdkWindow *win)
{
Im *im;
im = (Im*)ctx;
if(win != NULL && gdk_window_is_destroyed(win))
win = NULL;
if(parentim->set_client_window != NULL)
parentim->set_client_window(ctx, win);
if(im->win == win)
return;
if(win != NULL)
g_object_ref(win);
dropwindow(im);
im->win = win;
if(sendcaret(im) < 0)
srvclose(im);
}
static void
setcursorlocation(GtkIMContext *ctx, GdkRectangle *area)
{
Im *im;
im = (Im*)ctx;
if(parentim->set_cursor_location != NULL)
parentim->set_cursor_location(ctx, area);
if(im->win != NULL && gdk_window_is_destroyed(im->win))
dropwindow(im);
if(area == NULL || im->win == NULL)
im->cursorvalid = 0;
else{
im->cursor = *area;
im->cursorvalid = 1;
}
if(sendcaret(im) < 0)
srvclose(im);
}
static int
isprivate(GtkInputPurpose purpose)
{
return purpose == GTK_INPUT_PURPOSE_PASSWORD ||
purpose == GTK_INPUT_PURPOSE_PIN;
}
static void
purposechanged(GObject *obj, GParamSpec *pspec, gpointer data)
{
Im *im;
GtkInputPurpose purpose;
int private;
(void)pspec;
(void)data;
im = (Im*)obj;
g_object_get(obj, "input-purpose", &purpose, NULL);
private = isprivate(purpose);
if(private == im->private)
return;
im->private = private;
if(private)
reset(GTK_IM_CONTEXT(im));
}
static void
dispose(GObject *obj)
finalize(GObject *obj)
{
Im *im;
im = (Im*)obj;
srvdrop(im, 0);
dropwindow(im);
parentobject->dispose(obj);
if(im->fd >= 0)
close(im->fd);
G_OBJECT_CLASS(g_type_class_peek_parent(G_OBJECT_GET_CLASS(obj)))->finalize(obj);
}
static void
init(Im *im)
{
GtkInputPurpose purpose;
im->fd = -1;
im->usepreedit = 1;
im->pre[0] = '\0';
g_object_get(im, "input-purpose", &purpose, NULL);
im->private = isprivate(purpose);
g_signal_connect(im, "notify::input-purpose",
G_CALLBACK(purposechanged), NULL);
g_signal_connect(im, "commit", G_CALLBACK(simplecommit), im);
g_signal_connect(im, "preedit-end", G_CALLBACK(simpleend), im);
}
static void
@@ -550,16 +170,10 @@ classinit(ImClass *klass)
ic = GTK_IM_CONTEXT_CLASS(klass);
oc = G_OBJECT_CLASS(klass);
parentim = g_type_class_peek_parent(klass);
parentobject = G_OBJECT_CLASS(parentim);
ic->filter_keypress = kpress;
ic->get_preedit_string = getpreedit;
ic->reset = reset;
ic->focus_out = focusout;
ic->set_use_preedit = setusepreedit;
ic->set_client_window = setclientwindow;
ic->set_cursor_location = setcursorlocation;
oc->dispose = dispose;
oc->finalize = finalize;
}
static const GtkIMContextInfo info = {
@@ -584,8 +198,7 @@ im_module_init(GTypeModule *mod)
0,
(GInstanceInitFunc)init,
};
imtype = g_type_module_register_type(mod, GTK_TYPE_IM_CONTEXT_SIMPLE,
"strans-gtk", &ti, 0);
imtype = g_type_module_register_type(mod, GTK_TYPE_IM_CONTEXT, "strans-gtk", &ti, 0);
}
G_MODULE_EXPORT void

129
hash.c Normal file
View File

@@ -0,0 +1,129 @@
#include "dat.h"
#include "fn.h"
enum {
Tagsize = sizeof(Hnode),
};
static uvlong
hash(Str *s)
{
uvlong h;
int i;
h = 7;
for(i = 0; i < s->n; i++)
h = h*31 + s->r[i];
return h;
}
Hmap*
hmapalloc(int nbuckets, int size)
{
void *store;
Hmap *h;
int nsz;
nsz = Tagsize + size;
store = emalloc(sizeof(*h) + nbuckets * nsz);
h = store;
h->nbs = nbuckets;
h->nsz = nsz;
h->len = h->cap = nbuckets;
h->nodes = (uchar*)store + sizeof(*h);
return h;
}
static int
keycmp(Hnode *n, Str *key)
{
char buf[256];
int len;
len = stoutf(key, buf, sizeof(buf));
if(n->klen != len)
return 1;
return memcmp(n->key, buf, len);
}
Hnode*
hmapget(Hmap *h, Str *key)
{
Hnode *n;
uchar *v;
v = h->nodes + (hash(key) % h->nbs) * h->nsz;
for(;;){
n = (Hnode*)v;
if(n->filled && keycmp(n, key) == 0)
return n;
if(n->next == 0)
break;
v = h->nodes + n->next * h->nsz;
}
return nil;
}
static char*
sdup(Str *s)
{
char buf[256];
char *p;
int n;
n = stoutf(s, buf, sizeof(buf));
p = emalloc(n + 1);
memmove(p, buf, n);
p[n] = '\0';
return p;
}
int
hmapset(Hmap **store, Str *key, Str *val)
{
Hnode *n;
uchar *v;
Hmap *h;
int next;
vlong diff;
h = *store;
v = h->nodes + (hash(key) % h->nbs) * h->nsz;
for(;;){
n = (Hnode*)v;
next = n->next;
if(n->filled == 0)
goto replace;
if(keycmp(n, key) == 0)
goto replace;
if(next == 0)
break;
v = h->nodes + next * h->nsz;
}
if(h->cap == h->len){
diff = v - h->nodes;
h->cap *= 2;
*store = erealloc(*store, sizeof(*h) + h->cap * h->nsz);
h = *store;
h->nodes = (uchar*)*store + sizeof(*h);
v = h->nodes + diff;
n = (Hnode*)v;
}
n->next = h->len;
memset(h->nodes + h->len * h->nsz, 0, h->nsz);
h->len++;
v = h->nodes + n->next * h->nsz;
n = (Hnode*)v;
replace:
if(n->filled == 0){
n->key = sdup(key);
n->klen = strlen(n->key);
n->filled = 1;
}
n->next = next;
if(val->n > 0){
n->val = sdup(val);
n->vlen = strlen(n->val);
}
return 0;
}

1212
ibus.c

File diff suppressed because it is too large Load Diff

451
ipc.c
View File

@@ -1,451 +0,0 @@
#define _POSIX_C_SOURCE 200809L
#include <errno.h>
#include <poll.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>
#include "ipc.h"
static int64_t
nowms(void)
{
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (int64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
}
static int64_t
deadline(void)
{
return nowms() + Ipcwaitms;
}
static int
waitfd(int fd, short events, int64_t until)
{
struct pollfd pfd;
int64_t left;
int n;
pfd.fd = fd;
pfd.events = events;
for(;;){
left = until - nowms();
if(left <= 0){
errno = ETIMEDOUT;
return -1;
}
n = poll(&pfd, 1, left);
if(n < 0 && errno == EINTR)
continue;
if(n < 0)
return -1;
if(n == 0){
errno = ETIMEDOUT;
return -1;
}
return 0;
}
}
static int
readwait(int fd, void *buf, size_t n, int64_t until)
{
unsigned char *p;
ssize_t r;
p = buf;
while(n > 0){
r = recv(fd, p, n, MSG_DONTWAIT);
if(r < 0 && errno == EINTR)
continue;
if(r < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)){
if(waitfd(fd, POLLIN, until) < 0)
return -1;
continue;
}
if(r < 0)
return -1;
if(r == 0){
errno = ECONNRESET;
return -1;
}
p += r;
n -= r;
}
return 0;
}
static void
putlen(unsigned char p[Ipclensz], size_t n)
{
p[0] = n;
p[1] = n >> 8;
}
static size_t
getlen(const unsigned char p[Ipclensz])
{
return p[0] | (p[1] << 8);
}
static void
put32(unsigned char *p, int32_t v)
{
uint32_t u;
u = v;
p[0] = u;
p[1] = u >> 8;
p[2] = u >> 16;
p[3] = u >> 24;
}
static int32_t
get32(const unsigned char *p)
{
return (int32_t)((uint32_t)p[0] |
((uint32_t)p[1] << 8) |
((uint32_t)p[2] << 16) |
((uint32_t)p[3] << 24));
}
int
ipcpath(char *dst, size_t cap)
{
const char *dir, *sep;
int n;
dir = getenv("XDG_RUNTIME_DIR");
if(dir != NULL && dir[0] == '/'){
sep = dir[strlen(dir)-1] == '/' ? "" : "/";
n = snprintf(dst, cap, "%s%sstrans.sock", dir, sep);
}else
n = snprintf(dst, cap, "/tmp/strans.%lu",
(unsigned long)getuid());
if(n < 0 || (size_t)n >= cap)
return -1;
return 0;
}
/*
* A nonblocking AF_UNIX connect completes at once or fails with EAGAIN
* when the daemon's backlog is full; the socket stays nonblocking because
* every transfer below polls with a deadline.
*/
int
ipcconnect(void)
{
struct sockaddr_un addr;
int e, fd;
memset(&addr, 0, sizeof addr);
addr.sun_family = AF_UNIX;
if(ipcpath(addr.sun_path, sizeof addr.sun_path) < 0){
errno = ENAMETOOLONG;
return -1;
}
fd = socket(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0);
if(fd < 0)
return -1;
if(connect(fd, (struct sockaddr*)&addr, sizeof addr) < 0){
e = errno;
close(fd);
errno = e;
return -1;
}
return fd;
}
/*
* Engine key for an X keysym and its Unicode value: printable characters
* as themselves, the function keysyms 0xff00-0xffff as Kspec+offset, and
* no key at all for a modifier. The keypad keys and Shift+Tab
* (ISO_Left_Tab) fold onto their plain counterparts.
*/
uint32_t
ipckeysym(uint32_t sym, uint32_t unicode)
{
if(unicode >= ' ' && unicode != 0x7f)
return unicode;
if(sym >= 0xffe1 && sym <= 0xffee)
return 0;
switch(sym){
case 0xfe20:
case 0xff89:
return Ktab;
case 0xff8d:
return Kret;
case 0xff97:
return Kup;
case 0xff99:
return Kdown;
case 0xff9a:
return Kpgup;
case 0xff9b:
return Kpgdown;
}
if(sym >= 0xff00 && sym <= 0xffff)
return Kspec + (sym - 0xff00);
return unicode;
}
uint32_t
ipcmod(uint32_t state)
{
uint32_t m;
m = state & Mmask;
if(state & Msupervirt)
m |= Msuper;
return m;
}
void
ipcpackreq(unsigned char req[Ipcreqsz], int want, uint32_t mod,
uint32_t key)
{
req[0] = want ? Ipcreqwant : 0;
req[1] = mod;
req[2] = key;
req[3] = key >> 8;
req[4] = key >> 16;
req[5] = key >> 24;
}
void
ipcpackreset(unsigned char req[Ipcreqsz], int want)
{
memset(req, 0, Ipcreqsz);
req[0] = Ipcreqreset | (want ? Ipcreqwant : 0);
}
/* An old server sees this reserved frame as a key-zero request. */
void
ipcpackcap(unsigned char req[Ipcreqsz], int want)
{
memset(req, 0, Ipcreqsz);
req[0] = Ipcext | (want ? Ipcreqwant : 0);
req[1] = Ipcversion;
req[2] = Ipcopcap;
}
void
ipcpackcaret(unsigned char req[Ipccaretsz], int valid, int32_t x,
int32_t y, int32_t h)
{
memset(req, 0, Ipccaretsz);
req[0] = Ipcext;
req[1] = Ipcversion;
req[2] = Ipcopcaret;
req[3] = valid != 0;
if(valid){
put32(req + 4, x);
put32(req + 8, y);
put32(req + 12, h);
}
}
void
ipcpacksurround(unsigned char req[Ipcreqsz], size_t n)
{
memset(req, 0, Ipcreqsz);
req[0] = Ipcext;
req[1] = Ipcversion;
req[2] = Ipcopsurround;
putlen(req + 4, n);
}
size_t
ipcsurroundlen(const unsigned char req[Ipcreqsz])
{
return getlen(req + 4);
}
void
ipcunpackreq(const unsigned char req[Ipcreqsz], int *want, uint32_t *mod,
uint32_t *key)
{
*want = (req[0] & Ipcreqwant) != 0;
*mod = req[1] & Mmask;
*key = (uint32_t)req[2] |
((uint32_t)req[3] << 8) |
((uint32_t)req[4] << 16) |
((uint32_t)req[5] << 24);
}
int
ipcreqtype(const unsigned char req[Ipcreqsz])
{
if((req[0] & Ipcext) == 0)
return Ipckey;
if(req[1] != Ipcversion)
return Ipcunknown;
switch(req[2]){
case Ipcopcap:
if((req[0] & ~(Ipcext|Ipcreqwant)) != 0 ||
req[3] != 0 || req[4] != 0 || req[5] != 0)
return Ipcunknown;
return Ipccap;
case Ipcopcaret:
if(req[0] != Ipcext || req[3] > 1)
return Ipcunknown;
return Ipccaret;
case Ipcopsurround:
if(req[0] != Ipcext || req[3] != 0 ||
getlen(req + 4) > Ipcfieldmax)
return Ipcunknown;
return Ipcsurround;
}
return Ipcunknown;
}
int
ipcunpackcaret(const unsigned char req[Ipccaretsz], int *valid,
int32_t *x, int32_t *y, int32_t *h)
{
if(ipcreqtype(req) != Ipccaret || get32(req + 12) < 0)
return -1;
*valid = req[3];
*x = *valid ? get32(req + 4) : 0;
*y = *valid ? get32(req + 8) : 0;
*h = *valid ? get32(req + 12) : 0;
return 0;
}
int
ipcreqreset(const unsigned char req[Ipcreqsz])
{
return (req[0] & Ipcreqreset) != 0;
}
int
ipcpackresp(unsigned char *dst, size_t cap, int eaten, int del,
const char *commit, size_t ncommit, const char *preedit, size_t npreedit,
int want)
{
size_t n;
if(ncommit > Ipcfieldmax || npreedit > Ipcfieldmax ||
del < 0 || del > 0xff)
return -1;
n = Ipcresphdrsz + ncommit + (want ? Ipclensz + npreedit : 0);
if(cap < n)
return -1;
dst[0] = eaten != 0;
dst[1] = del;
putlen(dst + 2, ncommit);
memcpy(dst + Ipcresphdrsz, commit, ncommit);
if(want){
putlen(dst + Ipcresphdrsz + ncommit, npreedit);
memcpy(dst + Ipcresphdrsz + ncommit + Ipclensz, preedit, npreedit);
}
return n;
}
int
ipcreadn(int fd, void *buf, size_t n)
{
unsigned char *p;
ssize_t r;
p = buf;
while(n > 0){
r = read(fd, p, n);
if(r < 0 && errno == EINTR)
continue;
if(r <= 0)
return -1;
p += r;
n -= r;
}
return 0;
}
int
ipcsend(int fd, const void *buf, size_t n)
{
const unsigned char *p;
ssize_t r;
int64_t until;
p = buf;
until = deadline();
while(n > 0){
r = send(fd, p, n, MSG_NOSIGNAL|MSG_DONTWAIT);
if(r < 0 && errno == EINTR)
continue;
if(r < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)){
if(waitfd(fd, POLLOUT, until) < 0)
return -1;
continue;
}
if(r < 0)
return -1;
if(r == 0){
errno = EPIPE;
return -1;
}
p += r;
n -= r;
}
return 0;
}
/* Reads a length-prefixed field into dst[Ipcfieldmax+1]. */
static int
readfield(int fd, char *dst, int64_t until)
{
unsigned char len[Ipclensz];
size_t n;
if(readwait(fd, len, sizeof len, until) < 0)
return -1;
n = getlen(len);
if(n > Ipcfieldmax){
errno = EPROTO;
return -1;
}
if(readwait(fd, dst, n, until) < 0)
return -1;
dst[n] = '\0';
return n;
}
int
ipcreadresp(int fd, int want, char *commit, char *preedit, Ipcresp *resp)
{
unsigned char head[2];
int64_t until;
int n;
commit[0] = '\0';
preedit[0] = '\0';
memset(resp, 0, sizeof *resp);
until = deadline();
if(readwait(fd, head, sizeof head, until) < 0)
return -1;
if(head[0] > 1){
errno = EPROTO;
return -1;
}
resp->eaten = head[0];
resp->del = head[1];
n = readfield(fd, commit, until);
if(n < 0)
return -1;
resp->commitlen = n;
if(!want)
return 0;
n = readfield(fd, preedit, until);
if(n < 0)
return -1;
resp->preeditlen = n;
return 0;
}

99
ipc.h
View File

@@ -1,109 +1,20 @@
#include <stddef.h>
#include <stdint.h>
/*
* Key request: [flags, modifiers, key byte 0, ..., key byte 3].
* The want flag asks for the preedit text: the client then draws it and
* the popup does not. The reset flag is a lifecycle reset, not Escape.
* The server identifies the connection as the engine owner; that identity is
* not sent on the wire.
* Only Mmask modifier bits are retained.
* Response: [eaten, take-back, commit-length-low, commit-length-high,
* commit...], followed, when requested, by
* [preedit-length-low, preedit-length-high, preedit...].
* Take-back is the count of runes of the client's own text, just before the
* cursor, to remove before inserting the commit.
* Lengths are little-endian byte counts; fields are at most Ipcfieldmax bytes,
* so callers read them into char[Ipcfieldmax+1].
*
* Capability control is six bytes: [0x80|want, version, 0, 0, 0, 0]; the
* server always answers it eaten, which tells the client that extension
* frames are understood.
* Caret control is sixteen bytes: [0x80, version, 1, valid, x, y, h],
* with the signed coordinates and height encoded as little-endian 32-bit words.
* Surrounding-text control is six bytes, [0x80, version, 2, 0, length-low,
* length-high], and then that many bytes of the text just before the cursor.
* It changes what rides along with the requests that follow and is not
* answered.
*/
// req: 4 bytes [type, mod, key lo, key hi]
// resp: 2+n bytes [type, n, ...]
enum
{
Ipcreqwant = 1<<0,
Ipcreqreset = 1<<1,
Ipcext = 1<<7,
Ipcversion = 2,
Ipcopcap = 0,
Ipcopcaret = 1,
Ipcopsurround = 2,
Kspec = 0x110000,
Kspec = 0xf000,
Kback = Kspec|0x08,
Ktab = Kspec|0x09,
Kret = Kspec|0x0d,
Kesc = Kspec|0x1b,
Kup = Kspec|0x52,
Kdown = Kspec|0x54,
Kpgup = Kspec|0x55,
Kpgdown = Kspec|0x56,
Khangul = Kspec|0x31,
Khanja = Kspec|0x34,
Kmuhenkan = Kspec|0x22,
Khenkan = Kspec|0x23,
Kkana = Kspec|0x27,
Kzenkaku = Kspec|0x2a,
Kshift = Kspec|0xe1,
Ksuper = Kspec|0xeb,
/* X core modifier bits; GDK and IBus also flag Super at bit 26. */
Mshift = 1<<0,
Mctrl = 1<<2,
Malt = 1<<3,
Msuper = 1<<6,
Mmask = Mshift|Mctrl|Malt|Msuper,
Msupervirt = 1<<26,
Ipcreqsz = 6,
Ipccaretsz = 16,
Ipclensz = 2,
Ipcresphdrsz = 2 + Ipclensz,
Ipcfieldmax = 256,
Ipcmaxresp = Ipcresphdrsz + Ipcfieldmax + Ipclensz + Ipcfieldmax,
Ipcwaitms = 250,
};
enum
{
Ipcunknown = -1,
Ipckey,
Ipccap,
Ipccaret,
Ipcsurround,
};
typedef struct Ipcresp Ipcresp;
struct Ipcresp
{
int eaten;
int del; /* runes of the client's own text to take back first */
/* Bytes copied to each caller buffer, excluding its trailing NUL. */
size_t commitlen;
size_t preeditlen;
};
uint32_t ipckeysym(uint32_t, uint32_t);
uint32_t ipcmod(uint32_t);
void ipcpackreq(unsigned char[Ipcreqsz], int, uint32_t, uint32_t);
void ipcpackreset(unsigned char[Ipcreqsz], int);
void ipcpackcap(unsigned char[Ipcreqsz], int);
void ipcpackcaret(unsigned char[Ipccaretsz], int, int32_t, int32_t, int32_t);
void ipcpacksurround(unsigned char[Ipcreqsz], size_t);
size_t ipcsurroundlen(const unsigned char[Ipcreqsz]);
void ipcunpackreq(const unsigned char[Ipcreqsz], int*, uint32_t*, uint32_t*);
int ipcunpackcaret(const unsigned char[Ipccaretsz], int*, int32_t*, int32_t*, int32_t*);
int ipcreqtype(const unsigned char[Ipcreqsz]);
int ipcreqreset(const unsigned char[Ipcreqsz]);
int ipcpackresp(unsigned char*, size_t, int, int, const char*, size_t, const char*, size_t, int);
int ipcreadn(int, void*, size_t);
int ipcsend(int, const void*, size_t);
int ipcreadresp(int, int, char*, char*, Ipcresp*);
int ipcpath(char*, size_t);
int ipcconnect(void);

102
ko.c
View File

@@ -4,6 +4,8 @@
enum
{
Sbase = 0xAC00,
Jbase = 0x3131,
Jrange = 51,
Ncho = 19,
Njung = 21,
Njong = 28,
@@ -33,21 +35,41 @@ static Rune jong[] = {
L'', L'', L'',
};
/* A jamo's index in cho, jung, or jong, or -1; jong[0] is no jong. */
static int
idx(Rune *t, int n, Rune r)
{
int i;
static int choidx[Jrange] = {
[L''-Jbase] = 1, [L''-Jbase] = 2, [L''-Jbase] = 3,
[L''-Jbase] = 4, [L''-Jbase] = 5, [L''-Jbase] = 6,
[L''-Jbase] = 7, [L''-Jbase] = 8, [L''-Jbase] = 9,
[L''-Jbase] = 10, [L''-Jbase] = 11, [L''-Jbase] = 12,
[L''-Jbase] = 13, [L''-Jbase] = 14, [L''-Jbase] = 15,
[L''-Jbase] = 16, [L''-Jbase] = 17, [L''-Jbase] = 18,
[L''-Jbase] = 19,
};
for(i = 0; i < n; i++)
if(t[i] == r)
return i;
return -1;
}
static int jungidx[Jrange] = {
[L''-Jbase] = 1, [L''-Jbase] = 2, [L''-Jbase] = 3,
[L''-Jbase] = 4, [L''-Jbase] = 5, [L''-Jbase] = 6,
[L''-Jbase] = 7, [L''-Jbase] = 8, [L''-Jbase] = 9,
[L''-Jbase] = 10, [L''-Jbase] = 11, [L''-Jbase] = 12,
[L''-Jbase] = 13, [L''-Jbase] = 14, [L''-Jbase] = 15,
[L''-Jbase] = 16, [L''-Jbase] = 17, [L''-Jbase] = 18,
[L''-Jbase] = 19, [L''-Jbase] = 20, [L''-Jbase] = 21,
};
#define Choidx(r) idx(cho, Ncho, r)
#define Jungidx(r) idx(jung, Njung, r)
#define Jongidx(r) idx(jong, Njong, r)
static int jongidx[Jrange] = {
[L''-Jbase] = 2, [L''-Jbase] = 3, [L''-Jbase] = 4,
[L''-Jbase] = 5, [L''-Jbase] = 6, [L''-Jbase] = 7,
[L''-Jbase] = 8, [L''-Jbase] = 9, [L''-Jbase] = 10,
[L''-Jbase] = 11, [L''-Jbase] = 12, [L''-Jbase] = 13,
[L''-Jbase] = 14, [L''-Jbase] = 15, [L''-Jbase] = 16,
[L''-Jbase] = 17, [L''-Jbase] = 18, [L''-Jbase] = 19,
[L''-Jbase] = 20, [L''-Jbase] = 21, [L''-Jbase] = 22,
[L''-Jbase] = 23, [L''-Jbase] = 24, [L''-Jbase] = 25,
[L''-Jbase] = 26, [L''-Jbase] = 27, [L''-Jbase] = 28,
};
#define Choidx(r) (choidx[(r) - Jbase] - 1)
#define Jungidx(r) (jungidx[(r) - Jbase] - 1)
#define Jongidx(r) (jongidx[(r) - Jbase] - 1)
static Rune jamomap[128] = {
['r'] = L'', ['R'] = L'',
@@ -167,10 +189,6 @@ issyl(Rune r)
return r >= Sbase && r < Sbase + Ncho*Njung*Njong;
}
/*
* The pending text is at most one syllable; a key either extends it,
* commits it and starts another, or commits it and passes through.
*/
Emit
transko(Im *im, Rune c)
{
@@ -181,52 +199,44 @@ transko(Im *im, Rune c)
memset(&e, 0, sizeof e);
jm = keytojamo(c);
if(jm == 0){
e.s = im->pre;
if(im->pre.n > 0){
e.s = im->pre;
sclear(&im->pre);
}
sputr(&e.s, c);
return e;
}
e.eat = 1;
last = slastr(&im->pre);
if(last == 0){
sputr(&e.next, jm);
return e;
}
if(!issyl(last)){
ci = Choidx(last);
ji = Jungidx(jm);
if(ci >= 0 && ji >= 0){
spopr(&im->pre);
sputr(&e.next, compose(ci, ji, 0));
return e;
}
/* A vowel typed first is reordered under its consonant. */
if(Jungidx(last) >= 0 && Choidx(jm) >= 0){
sputr(&e.next, compose(Choidx(jm), Jungidx(last), 0));
return e;
}
/* Two vowels or two consonants may be one jamo, ㅘ or ㄳ. */
comb = combine(cvow, nelem(cvow), last, jm);
if(comb == 0)
comb = combine(cjong, nelem(cjong), last, jm);
if(comb){
sputr(&e.next, comb);
return e;
}
if(ji >= 0 && splitpair(cjong, nelem(cjong), last, &stay, &next)){
sputr(&e.s, stay);
sputr(&e.next, compose(Choidx(next), ji, 0));
return e;
if(Jungidx(last) >= 0 && ji >= 0){
comb = combine(cvow, nelem(cvow), last, jm);
if(comb){
spopr(&im->pre);
sputr(&e.next, comb);
return e;
}
}
e.s = im->pre;
sputr(&e.next, jm);
return e;
}
decompose(last, &ci, &ji, &joi);
if(joi == 0){
ni = Jongidx(jm);
if(ni > 0){
spopr(&im->pre);
sputr(&e.next, compose(ci, ji, ni));
return e;
}
@@ -235,17 +245,18 @@ transko(Im *im, Rune c)
comb = combine(cvow, nelem(cvow), jung[ji], jm);
if(comb){
ni = Jungidx(comb);
spopr(&im->pre);
sputr(&e.next, compose(ci, ni, 0));
return e;
}
}
}
if(joi > 0){
comb = combine(cjong, nelem(cjong), jong[joi], jm);
if(comb){
ni = Jongidx(comb);
if(ni > 0){
spopr(&im->pre);
sputr(&e.next, compose(ci, ji, ni));
return e;
}
@@ -256,19 +267,20 @@ transko(Im *im, Rune c)
if(splitpair(cjong, nelem(cjong), jc, &stay, &next)){
si = Jongidx(stay);
ni = Choidx(next);
spopr(&im->pre);
sputr(&e.s, compose(ci, ji, si));
sputr(&e.next, compose(ni, vi, 0));
return e;
}
ni = Choidx(jc);
if(ni >= 0){
spopr(&im->pre);
sputr(&e.s, compose(ci, ji, 0));
sputr(&e.next, compose(ni, vi, 0));
return e;
}
}
}
e.s = im->pre;
sputr(&e.next, jm);
return e;
@@ -283,10 +295,12 @@ backko(Im *im)
last = slastr(&im->pre);
if(!issyl(last)){
spopr(&im->pre);
if(splitpair(cvow, nelem(cvow), last, &stay, &next) ||
splitpair(cjong, nelem(cjong), last, &stay, &next))
if(splitpair(cvow, nelem(cvow), last, &stay, &next)){
spopr(&im->pre);
sputr(&im->pre, stay);
return;
}
spopr(&im->pre);
return;
}
decompose(last, &c, &j, &jo);

47
main.c
View File

@@ -3,11 +3,20 @@
Channel *drawc;
Channel *keyc;
Channel *dictreqc;
Channel *dictresc;
char *fontdir;
int
threadmaybackground(void)
{
return 1;
}
void
usage(void)
{
fprint(2, "usage: strans mapdir\n");
fprint(2, "usage: strans mapdir fontdir\n");
threadexitsall("usage");
}
@@ -29,9 +38,10 @@ emalloc(ulong n)
{
void *p;
p = mallocz(n, 1);
p = malloc(n);
if(p == nil)
die("out of memory");
memset(p, 0, n);
return p;
}
@@ -47,31 +57,20 @@ erealloc(void *p, ulong n)
void
threadmain(int argc, char **argv)
{
char *display, *scale;
if(argc != 2)
if(argc != 3)
usage();
/* Whichever popup runs is sized from it, and setfont reads Fontsz. */
scale = getenv("GDK_SCALE");
if(scale != nil && atoi(scale) > 1)
popupscale = min(atoi(scale), 4);
fontdir = argv[2];
drawc = chancreate(sizeof(Drawcmd), 4);
keyc = chancreate(sizeof(Keyreq), 0);
langinit(argv[1]);
composeinit();
srvinit();
dictreqc = chancreate(sizeof(Dictreq), 4);
dictresc = chancreate(sizeof(Dictres), 0);
mapinit(argv[1]);
dictinit(argv[1]);
proccreate(drawthread, nil, 16384);
proccreate(srvthread, nil, 16384);
proccreate(ibusthread, nil, 32768);
/* One popup per session: the Wayland frontend draws its own, so the
* X11 one and the XIM it serves stand down. */
if(getenv("WAYLAND_DISPLAY") != nil && wlinit())
proccreate(wlthread, nil, 32768);
else{
proccreate(drawthread, nil, 16384);
display = getenv("DISPLAY");
if(display != nil && display[0] != '\0')
proccreate(ximthread, nil, 32768);
}
imthread(nil);
threadcreate(dictthread, nil, 16384);
threadcreate(imthread, nil, 16384);
threadexits(nil);
}

View File

@@ -1,130 +1,12 @@
# Dictionary data
#
# The following are a set of tools to obtain and process dictionaries from the SKK project in order to use them with ktrans(1).
#
# grabskkdicts pulls the skk kana-kanji conversion dictionaries from the skk-dev/dict repo.
# skk2ktrans takes an skk dictionary and converts it into a kanji jisho suitable to be used with ktrans(1).
#
# You can fetch and convert all the dictionaries by running this file.
#
## Korean Hanja and symbol data
`hanja.src` and `mssymbol.src` are the tracked, reviewable sources for what
the Hanja search converts. Every data row is a result, a tab, and its
Hangul reading. A Hanja reads as a syllable or a word; a symbol reads as
the lone consonant a Korean keyboard's 한자 key offers it under:
```
漢 한
漢字 한자
※ ㅁ
```
`mkhanja` validates that contract and groups the rows of both sources by
their reading to produce the runtime dictionary. The two keyspaces do not
meet: a reading is either syllables or one jamo. Candidate order follows
source order. The sources keep all retained pairs for review; the
generated dictionary stores the first 128 candidates per reading because
that is the engine's lookup limit.
The sources are derived from libhangul release tag `libhangul-0.2.0`. The
annotated tag object is `20afc38922e3595ee3ed5b186f2ea05afe663763`, its
peeled commit is `41c702f5d3581325b646ef6249f1f641b0427ae0`, and
`data/hanja/hanja.txt` and `data/hanja/mssymbol.txt` have blob IDs
`199cfd70c4b306257ac2e018714a1285f7cf0ed3` and
`31c4e63d74293b6a759d4a2005cd1e7333746631`. Import and regenerate them
from the repository root with:
```sh
curl -L https://raw.githubusercontent.com/libhangul/libhangul/41c702f5d3581325b646ef6249f1f641b0427ae0/data/hanja/hanja.txt -o upstream
printf '%s %s\n' dd44dcc856cf542b1022d0f39c2e9b9f8805fdcc5923be80f04849ed97ce0996 upstream | sha256sum -c -
map/libhangul2hanja upstream >map/hanja.src
curl -L https://raw.githubusercontent.com/libhangul/libhangul/41c702f5d3581325b646ef6249f1f641b0427ae0/data/hanja/mssymbol.txt -o upstream
printf '%s %s\n' b685a4ebe2716b25eb29c42f6da6493716ecd78b604b44a33420987d21948e99 upstream | sha256sum -c -
map/libhangul2hanja upstream >map/mssymbol.src
map/mkhanja >map/hanja.dict
```
`libhangul2hanja` keeps a syllable reading only when its value is Hanja the
popup can draw: U+3400U+4DBF, U+4E00U+9FFF, or U+F900U+FAFF. Thus mixed
values and supplementary-plane ideographs are omitted. It keeps a jamo
reading, U+3131U+314E, only when its value is one rune the popup can draw
and a candidate row can carry — so `mssymbol.txt`'s ideographic space and
soft hyphen are omitted, leaving 985 of its 987 rows, and a Hanja under a
jamo reading is omitted as noise. The import preserves each upstream
license header and row order. The retained data is BSD 3-Clause licensed
by Choe Hwanjin; the complete text is in
`LICENSES/BSD-3-Clause-libhangul-hanja.txt`.
## Emoji and symbol data
`symbol.src` is hand-written: a symbol, a tab, and its ASCII aliases, kept
so that a bare `1`-`9` picks the matching superscript or subscript from a
`^` or `_` prefix search. `emoji.src` is generated: every fully-qualified
emoji of Unicode's `emoji-test.txt` except the skin-tone variants, with
its CLDR names and keywords in English, Korean, and Japanese. `mkemoji`
folds the aliases, adds each Japanese one in the other kana so a query
typed in either mode finds it, and writes one row per alias to
`emoji.dict`; the engine searches that dictionary by prefix, so the rows
carry no prefixes.
The sources are Emoji 17.0 and CLDR 48.2.0. Regenerate them from the
repository root with:
```sh
curl -L https://www.unicode.org/Public/17.0.0/emoji/emoji-test.txt -o emoji-test.txt
printf '%s %s\n' 1d8a944f88d7952f7ef7c5167fef3c67995bcae24543949710231b03a201acda emoji-test.txt | sha256sum -c -
for l in en ko ja; do
curl -L https://raw.githubusercontent.com/unicode-org/cldr-json/48.2.0/cldr-json/cldr-annotations-full/annotations/$l/annotations.json -o $l.json
curl -L https://raw.githubusercontent.com/unicode-org/cldr-json/48.2.0/cldr-json/cldr-annotations-derived-full/annotationsDerived/$l/annotations.json -o $l-derived.json
done
map/cldr2emoji emoji-test.txt en.json en-derived.json ko.json ko-derived.json ja.json ja-derived.json >map/emoji.src
map/mkemoji >map/emoji.dict
```
The data is under the Unicode License v3; the complete text is in
`LICENSES/Unicode-3.0.txt`.
## Japanese romaji
`hira.map` and `kata.map` are Mozc's default romaji table, one row per key,
in gojūon order; `kata.map` is the same table in Katakana, and
`make verify-map` checks that the two have the same keys. A doubled
consonant, or `tch`, maps to っ and keeps the consonant pending; the engine
knows that rule, the table only lists the rows.
## Japanese dictionary data
`kanji.dict` is the historical dictionary bundled with strans. Its own header
identifies it as the SKK Medium dictionary, version 8.1 of May 24, 1995,
rearranged for Plan 9 ktrans by Kenji Okamoto on February 17, 2000. It was
already present when this repository was created in commit
`dcd1147638f908eca7e4b7616815e215022cc99d` on December 23, 2025. No original
SKK input file or upstream revision was committed, so the current file cannot
honestly be regenerated byte-for-byte from repository artifacts. The license
notice in its header permits redistribution and modification under GPL version
2 or later.
The repository copy keeps that historical candidate order. Duplicate keys
were merged at their first occurrence, later unseen candidates were appended
in source order, duplicate candidates were removed, and the empty
`ようたつ` row was removed.
## Importing a current SKK dictionary
From the repository root, fetch a known upstream revision and convert it with:
```
git clone https://github.com/skk-dev/dict.git map/skkdicts
git -C map/skkdicts checkout --detach REVISION
map/skk2ktrans map/skkdicts/SKK-JISYO.M >map/kanji.dict.new
python3 map/verifymap.py map/kanji.dict.new
```
Use a full skk-dev/dict commit ID for `REVISION` and record it when replacing
the bundled data. Git is needed only to fetch upstream data; it is not part of
the normal build image.
`skk2ktrans` accepts one or more EUC-JP SKK files (or standard input), writes
UTF-8 tab-separated rows, and merges input in command-line and source order.
It strips annotations, deduplicates candidates, and omits candidates containing
whitespace. Rows containing expressions or escaped candidate delimiters are
rejected; rewrite or omit those rows before import.
`verifymap.py` checks UTF-8, row structure, unique keys, 64-rune keys and
values, canonical candidate spacing, and duplicate dictionary candidates.
Pass it the exact `.map` and `.dict` files used for runtime or validation.
The normal build does not install map data.
grabskkdicts
for(d in skkdicts/SKK-JISYO.*)
<$d skk2ktrans >$d.jisho

View File

@@ -1,52 +0,0 @@
#!/usr/bin/env python3
"""Write emoji.src: every fully-qualified emoji of Unicode's emoji-test.txt
but the skin-tone variants, each with its CLDR names and keywords in the
languages given, as result-first TAB-separated rows for mkemoji."""
import json
import sys
from pathlib import Path
def emoji(path):
out = []
for line in path.open(encoding="utf-8"):
if "; fully-qualified" not in line:
continue
cps = [int(cp, 16) for cp in line.split(";")[0].split()]
if any(0x1F3FB <= cp <= 0x1F3FF for cp in cps):
continue
out.append("".join(map(chr, cps)))
return out
def annotations(path):
"""CLDR annotations.json, plain or derived: emoji -> names, keywords."""
root = json.load(path.open(encoding="utf-8"))
table = root[next(iter(root))]["annotations"]
return {e: a.get("tts", []) + a.get("default", []) for e, a in table.items()}
def main():
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
if len(sys.argv) < 3:
print(f"usage: {sys.argv[0]} emoji-test.txt annotations.json...",
file=sys.stderr)
return 2
tables = [annotations(Path(arg)) for arg in sys.argv[2:]]
print("# Result first, then its CLDR names and keywords; see map/README.")
for e in emoji(Path(sys.argv[1])):
aliases = []
for table in tables:
for alias in table.get(e.replace("", ""), table.get(e, [])):
alias = alias.strip()
if alias and alias not in aliases:
aliases.append(alias)
if aliases:
print(e + "\t" + "\t".join(aliases))
return 0
if __name__ == "__main__":
sys.exit(main())

File diff suppressed because it is too large Load Diff

179
map/emoji.map Normal file
View File

@@ -0,0 +1,179 @@
! !
!! ⚠
!= ≠
* *
** ★
+ +
+- ±
- -
-> →
. .
.. ·
... …
./ ÷
: :
:( ☹
:) ☺
< <
<- ←
<= ≤
<> ≠
= =
== ≡
=> ⇒
> >
>= ≥
^ ^
^( ⁽
^) ⁾
^+ ⁺
^- ⁻
^= ⁼
_ _
_( ₍
_) ₎
_+ ₊
_- ₋
_= ₌
~ ~
~= ≈
^0 ⁰
_0 ₀
^1 ¹
_1 ₁
^2 ²
_2 ₂
<3 ♥
^3 ³
_3 ₃
^4 ⁴
_4 ₄
^5 ⁵
_5 ₅
^6 ⁶
_6 ₆
^7 ⁷
_7 ₇
^8 ⁸
_8 ₈
^9 ⁹
_9 ₉
_a ₐ
a a
al al
alp alp
alph alph
alpha α
b b
be be
bet bet
beta β
c c
ch ch
chi χ
d d
D D
de de
De Δ
deg °
del del
delt delt
delta δ
dn ↓
_e ₑ
e e
ep ep
eps ε
et et
eta η
g g
G G
ga ga
Ga Γ
gam gam
gamm gamm
gamma γ
^i ⁱ
i i
I I
II ∫
in in
inf ∞
io io
iot iot
iota ι
k k
ka ka
kap kap
kapp kapp
kappa κ
l l
L L
la la
La Λ
lam lam
lamb lamb
lambd lambd
lambda λ
m m
mu μ
mul ×
^n ⁿ
n n
nu ν
_o ₒ
o o
O O
om om
Om Ω
ome ome
omeg omeg
omega ω
oo ●
p p
P P
ph ph
Ph Φ
phi φ
pi π
Pi Π
PP ∏
ps ps
Ps Ψ
psi ψ
r r
rh rh
rho ρ
s s
S S
si si
Si Σ
sig sig
sigm sigm
sigma σ
sq √
SS ∑
t t
T T
ta ta
tau τ
th th
Th Θ
the the
thet thet
theta θ
u u
up ↑
ups υ
v v
vv ✓
_x ₓ
x x
X X
xi ξ
Xi Ξ
xx ✗
z z
ze ze
zet zet
zeta ζ

File diff suppressed because it is too large Load Diff

0
map/english.map Normal file
View File

2
map/grabskkdicts Executable file
View File

@@ -0,0 +1,2 @@
#!/bin/rc
git/clone https://github.com/skk-dev/dict skkdicts

0
map/hangul.map Normal file
View File

File diff suppressed because it is too large Load Diff

266539
map/hanja.src

File diff suppressed because it is too large Load Diff

View File

@@ -1,321 +1,170 @@
a あ
- ー
i い
u う
e え
o お
kya きゃ
kyi きぃ
kyu きゅ
kye きぇ
kyo きょ
ka か
ki き
ku く
ke け
ko こ
kwa くぁ
kwi くぃ
kwu くぅ
kwe くぇ
kwo くぉ
gya ぎゃ
gyi ぎぃ
gyu ぎゅ
gye ぎぇ
gyo ぎょ
ga が
ki き
kya きゃ
kyu きゅ
kyo きょ
gi ぎ
gya ぎゃ
gyu ぎゅ
gyo ぎょ
ku く
gu ぐ
ke け
ge げ
ko こ
go ご
gwa ぐぁ
gwi ぐぃ
gwu ぐぅ
gwe ぐぇ
gwo ぐぉ
sya しゃ
syi しぃ
syu しゅ
sye しぇ
syo しょ
sha しゃ
sa さ
za ざ
si し
shi し
sha しゃ
shu しゅ
she しぇ
sho しょ
sa さ
si
su す
se せ
so そ
swa すぁ
swi すぃ
swu すぅ
swe すぇ
swo すぉ
z/ ・
z. …
z, ‥
zh ←
zj ↓
zk ↑
zl →
z- 〜
z[ 『
z] 』
zya じゃ
zyi じぃ
zyu じゅ
zye じぇ
zyo じょ
zwa ずぁ
zwi ずぃ
zwu ずぅ
zwe ずぇ
zwo ずぉ
za ざ
zi じ
zu ず
ze ぜ
zo ぞ
syo しょ
zi
ja じゃ
ji じ
ju じゅ
je じぇ
jo じょ
jya じゃ
jyi じぃ
jyu じゅ
jye じぇ
jyo じょ
tya ちゃ
tyi ちぃ
tyu ちゅ
tye ちぇ
tyo ちょ
tsa つぁ
tsi つぃ
tse つぇ
tso つぉ
tha てゃ
thi てぃ
t'i てぃ
thu てゅ
the てぇ
tho てょ
t'yu てゅ
twa とぁ
twi とぃ
twu とぅ
twe とぇ
two とぉ
t'u とぅ
su す
zu ず
se せ
ze ぜ
so そ
zo ぞ
ta た
da だ
ti ち
chi ち
cha ちゃ
chu ちゅ
tyu ちゅ
cho ちょ
ji じ
tu つ
tsu つ
te て
to と
dya ぢゃ
dyi ぢぃ
dyu ぢゅ
dye ぢぇ
dyo ぢょ
dha でゃ
dhi でぃ
d'i でぃ
dhu でゅ
dhe でぇ
dho でょ
d'yu でゅ
dwa どぁ
dwi どぃ
dwu どぅ
dwe どぇ
dwo どぉ
d'u どぅ
da だ
di ぢ
du づ
te て
de で
to と
do ど
cha ちゃ
chi ち
chu ちゅ
che ちぇ
cho ちょ
cya ちゃ
cyi ちぃ
cyu ちゅ
cye ちぇ
cyo ちょ
ca か
ci し
cu く
ce せ
co こ
nya にゃ
nyi にぃ
nyu にゅ
nye にぇ
nyo にょ
n' ん
nn ん
n ん
na な
ni に
nya にゃ
nyu にゅ
nyo にょ
nu ぬ
ne ね
no の
hya ひゃ
hyi ひぃ
hyu ひゅ
hye ひぇ
hyo ひょ
hwa ふぁ
hwi ふぃ
hwe ふぇ
hwo ふぉ
hwyu ふゅ
ha は
hi ひ
hu ふ
he へ
ho ほ
fa ふぁ
fi ふぃ
fu ふ
fe ふぇ
fo ふぉ
fya ふゃ
fyu ふゅ
fyo ふょ
bya びゃ
byi びぃ
byu びゅ
bye びぇ
byo びょ
ba ば
bi び
bu ぶ
be べ
bo ぼ
pya ぴゃ
pyi ぴぃ
pyu ぴゅ
pye ぴぇ
pyo ぴょ
va ば
pa ぱ
hi ひ
hya ひゃ
hyu ひゅ
hyo ひょ
bi び
bya びゃ
byu びゅ
byo びょ
vi び
pi ぴ
pya ぴゃ
pyu ぴゅ
pyo ぴょ
hu ふ
fu ふ
bu ぶ
vu ぶ
pu ぷ
he へ
be べ
ve べ
pe ぺ
ho ほ
bo ぼ
vo ぼ
po ぽ
mya みゃ
myi みぃ
myu みゅ
mye みぇ
myo みょ
ma ま
mi み
mya みゃ
myu みゅ
myo みょ
mu む
me め
mo も
ye いぇ
ya や
yu ゆ
yo よ
rya りゃ
ryi りぃ
ryu りゅ
rye りぇ
ryo りょ
ra ら
ri り
rya りゃ
ryu りゅ
ryo りょ
ru る
re れ
ro ろ
wu う
wyi ゐ
wye ゑ
wa わ
wi うぃ
we うぇ
wi
we
wo を
wha うぁ
whi うぃ
whu う
whe うぇ
who うぉ
va ゔぁ
vi ゔぃ
vu ゔ
ve ゔぇ
vo ゔぉ
vya ゔゃ
vyi ゔぃ
vyu ゔゅ
vye ゔぇ
vyo ゔょ
qa くぁ
qi くぃ
qu く
qe くぇ
qo くぉ
n ん
xn ん
xa ぁ
xi ぃ
xu ぅ
xe ぇ
xo ぉ
xyi ぃ
xye ぇ
xka ヵ
xke ヶ
xtu っ
xtsu っ
xya ゃ
xyu ゅ
xyo ょ
xwa ゎ
la ぁ
li ぃ
lu ぅ
le
lo ぉ
lyi ぃ
lye ぇ
lka ヵ
lke ヶ
ltu
ltsu
lya ゃ
lyu ゅ
lyo ょ
lwa ゎ
qq っ
vv っ
ll
xx
kk
gg
ss
zz
jj
tt
tch
dd っ
hh っ
ff っ
bb
pp っ
mm
yy っ
rr っ
ww っ
cc っ
- ー
~ 〜
kka っか
kki っき
kkya っきゃ
kkyu っきゅ
kkyo っきょ
kku っ
kke っけ
kko っこ
ssa っさ
ssi っし
sshi っし
ssha っしゃ
sshu っしゅ
sshe っし
ssho っしょ
ssyo っしょ
ssu っす
sse っせ
sso っそ
tta
dda
tti っち
cchi っち
ccha っちゃ
cchu っちゅ
ttyu っちゅ
ccho っちょ
ttu
ttsu
ddu
tte
dde
tto
ddo
ppa
ppi
ppya っぴゃ
ppyu っぴゅ
ppyo っぴょ
ppu
ppe
ppo
. 。
, 、
[ 「
] 」

28
map/judou.map Normal file
View File

@@ -0,0 +1,28 @@
 
,
. 。
< 《
> 》
/
?
;
:
\ 、
| ・
`
~ 〜
!
@
#
$ ¥
&
*
(
)
-
+
=
[ 「
] 」
{ 『
} 』

View File

@@ -60,6 +60,7 @@
あたらs 新
あつi 熱 暑 厚
あつk 厚 熱 暑
あつk 厚 熱 暑
あつs 暑
あつm 集 厚
あつかe 扱
@@ -349,6 +350,7 @@
かわいr 可愛
かんj 感
かんs 関
かんj 感
かんz 感
かんがe 考
かんしゃs 感謝
@@ -453,7 +455,7 @@
こu 乞 請
こy 来
こうj 高
こうしょう 鉱床 高尚 交渉
こうしょう 鉱床 高尚
こうぶつ 鉱物 好物
こうりょs 考慮
こえt 超
@@ -663,6 +665,8 @@
ただc 直
ただt 直
たとe 例
たおr 倒
たおr 倒
たのm 頼
たのn 頼
たのs 楽
@@ -738,6 +742,7 @@
つみあげr 積み上げ
つみあげt 積み上げ
つめt 冷
つづr 綴
つよi 強
つよk 強
つらi 辛
@@ -788,6 +793,7 @@
とt 取 撮 採
とu 問
とw 問
とj 閉
といあw 問い合
とうj 投
とおi 遠
@@ -907,12 +913,13 @@
ねらt 狙
ねらu 狙
のb 述 伸 延
のk 退
のk 退
のm 飲 呑
のn 飲
のr 乗 載
のs 載 乗
のt 乗 載
のk 乗
のこr 残
のこs 残
のこt 残
@@ -1052,11 +1059,12 @@
ぼうえいs 防衛
まc 待
まi 舞
まj 交
まj 交
まk 負 巻
まs 増
まt 待
まu 舞
まj 混
まz 混 先
まいr 参 詣
まいt 参 詣
@@ -1233,6 +1241,8 @@
よわm 弱
よわs 弱
よわt 弱
よろこb 喜 慶
よろこb 喜 慶
ろんj 論
ろんz 論
わk 分 湧
@@ -1928,7 +1938,7 @@
かいもの 買物
かいよう 海洋 潰瘍
かいらん 回覧
かいり 解離 乖離 海里 浬
かいり 解離 乖離 海里 浬
かいりょう 改良
かいれい 海嶺
かいろ 回路
@@ -2257,6 +2267,7 @@
がくわり 学割
がけ 崖
がし 樫
かせい 火星
がそ 画素
がそかん 画素間
がそごと 画素毎
@@ -2982,6 +2993,7 @@
こうしゃ 後者 公社 校舎
こうしゅう 講習 公衆
こうしゅうかい 講習会
こうしょう 交渉 鉱床
こうしん 更新 交信 後身
こうじ 麹 公示 高次 工事 孝二
こうじつ 口実
@@ -3497,7 +3509,7 @@
しこく 四国
しご 死後
しごと 仕事
しごとば 仕事場 堺市学園町1ー1
しごとば 仕事場 堺市学園町1ー1
しさ 示唆 視差
しさい 仔細 子細
しさく 試作
@@ -4790,7 +4802,7 @@
たいきゅうりょく 耐久力
たいきょく 対極
たいきん 大金
たいく 体躯 体育
たいく 体躯
たいくつ 退屈
たいけい 体系 体型
たいけん 体験
@@ -4937,6 +4949,8 @@
たんらく 短絡
だ 騨 駄 陀 楕 舵 柁 打 惰 妥 堕 唾 田 朶
だい 大 第 内 代 台 題 醍
たいく 体育
たいいく 体育
だいいち 第一
だいがく 大学
だいがくいん 大学院
@@ -5659,7 +5673,7 @@
どんき 鈍器
どんてん 曇天
どんよく 貪欲
な 名 七 那 奈 魚 菜 字 肴 納 無 儺 己 汝 何
な 名 七 那 奈 魚 菜 字 肴 納 無 儺 己 汝 何
ない 内 無
ないがい 内外
ないき 内記 内規
@@ -6802,7 +6816,7 @@
ぼうし 防止
ぼうそう 暴走
ぼうだい 膨大
ぼうちょう 膨張 防潮 傍聴
ぼうちょう 膨張 防潮 傍聴
ぼうとう 冒頭
ぼうどう 暴動
ぼうねんかい 忘年会
@@ -7300,6 +7314,7 @@
よこう 予行 予稿
よこうち 横内
よこく 予告
ようたつ
よこて 横手
よこはま 横浜
よこもじ 横文字
@@ -7411,7 +7426,7 @@
りゅうど 粒度
りゆう 理由
りょ 虜 旅 慮 侶
りょう 量 寮 両 領 陵 遼 諒 良 糧 稜 瞭 療 猟 涼 梁 料 凌 僚 亮 了 漁 粮 霊
りょう 量 寮 両 領 陵 遼 諒 良 糧 稜 瞭 療 猟 涼 梁 料 凌 僚 亮 了 漁 粮 霊
りょうあし 両足
りょういき 領域
りょうか 量化

View File

@@ -1,321 +1,142 @@
a ア
- ー
i イ
u ウ
e エ
o オ
kya キャ
kyi キィ
kyu キュ
kye キェ
kyo キョ
ka カ
ki キ
ku ク
ke ケ
ko コ
kwa クァ
kwi クィ
kwu クゥ
kwe クェ
kwo クォ
gya ギャ
gyi ギィ
gyu ギュ
gye ギェ
gyo ギョ
ga ガ
ki キ
kya キャ
kyu キュ
kyo キョ
gi ギ
gya ギャ
gyu ギュ
gyo ギョ
ku ク
gu グ
ke ケ
ge ゲ
ko コ
go ゴ
gwa グァ
gwi グィ
gwu グゥ
gwe グェ
gwo グォ
sya シャ
syi シィ
syu シュ
sye シェ
syo ショ
sha シャ
shi シ
shu シュ
she シェ
sho ショ
sa サ
si シ
su ス
se セ
so ソ
swa スァ
swi スィ
swu スゥ
swe スェ
swo スォ
z/ ・
z. …
z, ‥
zh ←
zj ↓
zk ↑
zl →
z- 〜
z[ 『
z] 』
zya ジャ
zyi ジィ
zyu ジュ
zye ジェ
zyo ジョ
zwa ズァ
zwi ズィ
zwu ズゥ
zwe ズェ
zwo ズォ
za ザ
zi
zu ズ
ze ゼ
zo ゾ
ja ジャ
si
shi シ
ji ジ
sha シャ
she シェ
shu シュ
je ジェ
sho ショ
syo ショ
ja ジャ
ju ジュ
je ジェ
jo ジョ
jya ジャ
jyi ジィ
jyu ジュ
jye ジェ
jyo ジョ
tya チャ
tyi チィ
tyu チュ
tye チェ
tyo チョ
tsa ツァ
tsi ツィ
tse ツェ
tso ツォ
tha テャ
thi ティ
t'i ティ
thu テュ
the テェ
tho テョ
t'yu テュ
twa トァ
twi トィ
twu トゥ
twe トェ
two トォ
t'u トゥ
su ス
zu ズ
se セ
ze ゼ
so ソ
zo ゾ
ta タ
ti チ
tu ツ
tsu ツ
te テ
to ト
dya ヂャ
dyi ヂィ
dyu ヂュ
dye ヂェ
dyo ヂョ
dha デャ
dhi ディ
d'i ディ
dhu デュ
dhe デェ
dho デョ
d'yu デュ
dwa ドァ
dwi ドィ
dwu ドゥ
dwe ドェ
dwo ドォ
d'u ドゥ
da ダ
di
du ヅ
de デ
do ド
cha チャ
ti ティ
chi チ
zi ヂ
cha チャ
chu チュ
tyu チュ
che チェ
cho チョ
cya チャ
cyi チィ
cyu チュ
cye チェ
cyo チョ
ca カ
ci シ
cu ク
ce セ
co コ
nya ニャ
nyi ニィ
nyu ニュ
nye ニェ
nyo ニョ
n' ン
nn ン
n ン
tu ツ
tsu ツ
du ヅ
te テ
de デ
to ト
do ド
na ナ
ni ニ
nya ニャ
nyu ニュ
nyo ニョ
nu ヌ
ne ネ
no
hya ヒャ
hyi ヒィ
hyu ヒュ
hye ヒェ
hyo ヒョ
hwa ファ
hwi フィ
hwe フェ
hwo フォ
hwyu フュ
ha ハ
hi ヒ
hu フ
he ヘ
ho ホ
fa ファ
fi フィ
fu フ
fe フェ
fo フォ
fya フャ
fyu フュ
fyo フョ
bya ビャ
byi ビィ
byu ビュ
bye ビェ
byo ビョ
ba バ
bi ビ
bu ブ
be ベ
bo ボ
pya ピャ
pyi ピィ
pyu ピュ
pye ピェ
pyo ピョ
pa パ
hi ヒ
hya ヒャ
hyu ヒュ
hyo ヒョ
bi ビ
bya ビャ
byu ビュ
byo ビョ
pi ピ
pya ピャ
pyu ピュ
pyo ピョ
hu フ
fu フ
bu ブ
pu プ
he ヘ
be ベ
pe ペ
ho ホ
bo ボ
po ポ
mya ミャ
myi ミィ
myu ミュ
mye ミェ
myo ミョ
ma マ
mi ミ
mya ミャ
myu ミュ
myo ミョ
mu ム
me メ
mo モ
ye イェ
ya ヤ
yu ユ
yo ヨ
rya リャ
ryi リィ
ryu リュ
rye リェ
ryo リョ
ra ラ
ri リ
rya リャ
ryu リュ
ryo リョ
ru ル
re レ
ro ロ
wu ウ
wyi ヰ
wye ヱ
wa ワ
wi ウィ
we ウェ
wi
we
wo ヲ
wha ウァ
whi ウィ
whu ウ
whe ウェ
who ウォ
va ヴァ
vi ヴィ
vu ヴ
ve ヴェ
vo ヴォ
vya ヴャ
vyi ヴィ
vyu ヴュ
vye ヴェ
vyo ヴョ
qa クァ
qi クィ
qu ク
qe クェ
qo クォ
n ン
xn ン
v ヴ
xa ァ
xi ィ
xu ゥ
xe ェ
xo ォ
xyi ィ
xye ェ
xka ヵ
xke ヶ
xtu ッ
xtsu ッ
xya ャ
xyu ュ
xyo ョ
xwa ヮ
la ァ
li ィ
lu ゥ
le ェ
lo ォ
lyi ィ
lye ェ
lka ヵ
lke ヶ
ltu ッ
ltsu ッ
lya ャ
lyu ュ
lyo ョ
lwa ヮ
qq ッ
vv ッ
ll ッ
xx ッ
cc ッ
dd ッ
kk ッ
gg
ss ッ
zz ッ
jj ッ
pp
tt ッ
tch ッ
dd
hh ッ
ff ッ
bb ッ
pp ッ
mm ッ
yy ッ
rr ッ
ww ッ
cc ッ
- ー
~ 〜
ss
xn ン
di ディ
fa ファ
fi フィ
fe フェ
fo フォ
va ヴァ
vi ヴィ
ve ヴェ
vo ヴォ
. 。
, 、
[ 「
] 」

View File

@@ -1,98 +0,0 @@
#!/usr/bin/env python3
"""Extract Hanja and symbol readings from libhangul data."""
import sys
import unicodedata
from pathlib import Path
def ishangul(s):
return s != "" and all(0xAC00 <= ord(c) <= 0xD7A3 for c in s)
def isjamo(s):
return len(s) == 1 and 0x3131 <= ord(s) <= 0x314E
def ishanja(s):
return s != "" and all(0x3400 <= ord(c) <= 0x4DBF
or 0x4E00 <= ord(c) <= 0x9FFF
or 0xF900 <= ord(c) <= 0xFAFF for c in s)
def issymbol(s):
"""One rune the popup can draw and a candidate row can carry: not a
space, which the row separates candidates with, and not a formatting
character, which would leave a blank candidate to pick."""
return (len(s) == 1 and not s.isspace() and not ishanja(s)
and not unicodedata.category(s).startswith("C"))
def keeps(reading, value):
"""A syllable reading gives Hanja; a jamo reading gives a symbol."""
if ishangul(reading):
return ishanja(value)
return isjamo(reading) and issymbol(value)
def extract(src, name):
comments = []
entries = []
seen = set()
leading = True
for lineno, raw in enumerate(src, 1):
line = raw.rstrip("\r\n")
if not line:
continue
if line.startswith("#"):
if leading:
comments.append(line.rstrip())
continue
leading = False
fields = line.split(":")
if len(fields) != 3:
raise ValueError(f"{name}:{lineno}: need key:value:comment")
reading, value, _ = fields
if not keeps(reading, value):
continue
pair = (value, reading)
if pair in seen:
raise ValueError(f"{name}:{lineno}: duplicate reading")
seen.add(pair)
entries.append(pair)
if not entries:
raise ValueError(f"{name}: no readings")
return comments, entries
def main():
sys.stdin.reconfigure(encoding="utf-8")
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
if len(sys.argv) > 2:
print(f"usage: {sys.argv[0]} [libhangul-hanja.txt]", file=sys.stderr)
return 2
src = sys.stdin
name = "<stdin>"
try:
if len(sys.argv) == 2:
name = sys.argv[1]
src = Path(name).open(encoding="utf-8")
comments, entries = extract(src, name)
for line in comments:
print(line)
if comments:
print()
for value, reading in entries:
print(f"{value}\t{reading}")
except (OSError, UnicodeError, ValueError) as error:
print(error, file=sys.stderr)
return 1
finally:
if src is not sys.stdin:
src.close()
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -1,86 +1,39 @@
#!/usr/bin/env python3
"""Write emoji.dict to stdout from result-first UTF-8 TSV files."""
#!/bin/sh
import sys
import unicodedata
from pathlib import Path
awk '
BEGIN { FS = "\t" }
/^$/ { next }
{
key = $1
val = $2
map[key] = val
for(i = 1; i < length(key); i++){
p = substr(key, 1, i)
if(!(p in map))
map[p] = p
}
}
END {
for(k in map)
print k "\t" map[k]
}
' $1 | sort -k1,1 > emoji.map
SOURCES = [Path(__file__).with_name(name)
for name in ("symbol.src", "emoji.src")]
MAXRUNES = 64
MAXCANDIDATES = 128
HIRA = {c: c + 0x60 for c in range(0x3041, 0x3097)}
KATA = {c: c - 0x60 for c in range(0x30A1, 0x30F7)}
def fold(s):
s = "".join(chr(ord(c) + 32) if "A" <= c <= "Z" else c for c in s)
return unicodedata.normalize("NFC", s)
def kana(alias):
"""The alias, and in the other kana where it has any: a query typed
in either Japanese mode finds it."""
return {alias, alias.translate(HIRA), alias.translate(KATA)}
def hascontrol(s):
return any(unicodedata.category(c) == "Cc" for c in s)
def read(path):
entries = []
with path.open(encoding="utf-8") as src:
for lineno, raw in enumerate(src, 1):
line = raw.rstrip("\r\n")
if not line.strip():
continue
if "\t" not in line and line.lstrip().startswith("#"):
continue
fields = line.split("\t")
if len(fields) < 2:
raise ValueError(f"{path}:{lineno}: need a result and an alias")
result = unicodedata.normalize("NFC", fields[0])
if (not result or len(result) > MAXRUNES or hascontrol(result)
or any(c.isspace() for c in result)):
raise ValueError(f"{path}:{lineno}: bad result")
for field in fields[1:]:
alias = fold(field)
if (not alias or len(alias) > MAXRUNES or hascontrol(alias)
or alias != alias.strip() or alias.startswith(";")):
raise ValueError(f"{path}:{lineno}: bad alias")
entries.extend((result, a) for a in sorted(kana(alias)))
return entries
def build(entries):
"""One row per alias, in order of first appearance; the engine searches
the dictionary by prefix, so no prefix rows are needed."""
table = {}
for result, alias in entries:
values = table.setdefault(alias, [])
if result not in values:
values.append(result)
for alias, values in table.items():
yield f"{alias}\t{' '.join(values[:MAXCANDIDATES])}"
def main():
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
paths = [Path(arg) for arg in sys.argv[1:]] or SOURCES
try:
entries = [entry for path in paths for entry in read(path)]
for line in build(entries):
print(line)
except (OSError, UnicodeError, ValueError) as error:
print(error, file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
sys.exit(main())
awk '
BEGIN { FS = "\t" }
/^$/ { next }
{
key = $1
val = $2
for(i = 1; i <= length(key); i++){
p = substr(key, 1, i)
dict[p] = dict[p] " " val
}
}
END {
for(p in dict){
sub(/^ /, "", dict[p])
print p "\t" dict[p]
}
}
' $1 | sort -k1,1 > emoji.dict

View File

@@ -1,96 +0,0 @@
#!/usr/bin/env python3
"""Write hanja.dict from result-per-row UTF-8 sources."""
import sys
import unicodedata
from pathlib import Path
SOURCES = [Path(__file__).with_name(name)
for name in ("hanja.src", "mssymbol.src")]
MAXCANDIDATES = 128
def ishangul(s):
return s != "" and all(0xAC00 <= ord(c) <= 0xD7A3 for c in s)
def isjamo(s):
return len(s) == 1 and 0x3131 <= ord(s) <= 0x314E
def ishanja(s):
return s != "" and all(0x3400 <= ord(c) <= 0x4DBF
or 0x4E00 <= ord(c) <= 0x9FFF
or 0xF900 <= ord(c) <= 0xFAFF for c in s)
def issymbol(s):
"""One rune the popup can draw and a candidate row can carry: not a
space, which the row separates candidates with, and not a formatting
character, which would leave a blank candidate to pick."""
return (len(s) == 1 and not s.isspace() and not ishanja(s)
and not unicodedata.category(s).startswith("C"))
def read(path, table, seen):
"""Adds path's rows to table, keyed by reading, and returns its header."""
comments = []
leading = True
found = False
with path.open(encoding="utf-8") as src:
for lineno, raw in enumerate(src, 1):
line = raw.rstrip("\r\n")
if not line:
continue
if line.startswith("#"):
if leading:
comments.append(";;" + line[1:])
continue
leading = False
fields = line.split("\t")
if len(fields) != 2:
raise ValueError(f"{path}:{lineno}: need result<TAB>reading")
value, reading = fields
if ishangul(reading):
if not ishanja(value):
raise ValueError(f"{path}:{lineno}: need BMP Hanja")
elif isjamo(reading):
if not issymbol(value):
raise ValueError(f"{path}:{lineno}: need one symbol rune")
else:
raise ValueError(f"{path}:{lineno}: need a syllable or jamo "
"reading")
pair = (value, reading)
if pair in seen:
raise ValueError(f"{path}:{lineno}: duplicate reading")
seen.add(pair)
found = True
table.setdefault(reading, []).append(value)
if not found:
raise ValueError(f"{path}: no readings")
return comments
def main():
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
paths = [Path(arg) for arg in sys.argv[1:]] or SOURCES
table = {}
seen = set()
try:
comments = [line for path in paths for line in read(path, table, seen)]
for line in comments:
print(line)
if comments:
print()
for reading, candidates in table.items():
print(f"{reading}\t{' '.join(candidates[:MAXCANDIDATES])}")
except (OSError, UnicodeError, ValueError) as error:
print(error, file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -1,9 +1,5 @@
#!/usr/bin/env python3
import sys
sys.stdout.reconfigure(encoding="utf-8")
# tone marks: s=rising f=falling r=hook x=tilde j=dot
tone = {
's': str.maketrans("aăâeêioôơuưy", "áắấéếíóốớúứý"),
@@ -28,21 +24,21 @@ modcons = [
("dd", "đ"),
]
upper = str.maketrans(
"aăâeêioôơuưyđáắấéếíóốớúứýàằầèềìòồờùừỳảẳẩẻểỉỏổởủửỷãẵẫẽễĩõỗỡũữỹạặậẹệịọộợụựỵ",
"AĂÂEÊIOÔƠUƯYĐÁẮẤÉẾÍÓỐỚÚỨÝÀẰẦÈỀÌÒỒỜÙỪỲẢẲẨẺỂỈỎỔỞỦỬỶÃẴẪẼỄĨÕỖỠŨỮỸẠẶẬẸỆỊỌỘỢỤỰỴ")
def addtone(v, t):
return v.translate(tone[t])
entries = []
def initialupper(s):
return s[0].upper() + s[1:]
def emit(src, dst):
entries.append((src, dst))
print(f"{src}\t{dst}")
usrc = initialupper(src)
udst = initialupper(dst)
if (usrc, udst) != (src, dst):
print(f"{usrc}\t{udst}")
def emit(input, output):
print(f"{input}\t{output}")
def up(s):
c = s[0].translate(upper)
if c == s[0]:
c = s[0].upper()
return c + s[1:]
print(f"{up(input)}\t{up(output)}")
def vowel1():
for v in "aeiouy":
@@ -55,15 +51,11 @@ def vowel2():
tab = [
("oa", "oa", "a"), ("oe", "oe", "e"), ("ai", "ai", "a"),
("ao", "ao", "a"), ("au", "au", "a"), ("ay", "ay", "a"),
("eo", "eo", "e"), ("eu", "eu", "e"), ("ia", "ia", "i"),
("iu", "iu", "i"), ("oi", "oi", "o"), ("ua", "ua", "u"),
("eu", "eu", "e"), ("iu", "iu", "i"), ("oi", "oi", "o"),
("ui", "ui", "u"), ("uy", "uy", "y"),
("aau", "âu", "â"), ("aay", "ây", "â"), ("eeu", "êu", "ê"),
("ooi", "ôi", "ô"), ("owi", "ơi", "ơ"), ("uwi", "ưi", "ư"),
("uwu", "ưu", "ư"), ("uwa", "ưa", "ư"), ("uee", "", "ê"),
("iee", "", "ê"), ("yee", "", "ê"), ("uoo", "", "ô"),
("uow", "ươ", "ơ"), ("uaa", "", "â"), ("oaw", "", "ă"),
("uwow", "ươ", "ơ"),
("uwa", "ưa", "a"), ("uwow", "ươ", "ơ"),
]
for i, o, v in tab:
emit(i, o)
@@ -72,21 +64,21 @@ def vowel2():
emit("ie", "ie")
emit("ye", "ye")
emit("uo", "uo")
emit("ua", "ua")
def vowel3():
# input, output, vowel
tab = [
("ieeu", "iêu", "ê"), ("yeeu", "yêu", "ê"),
("uooi", "uôi", "ô"), ("uowi", "ươi", "ơ"),
("uowu", "ươu", "ơ"), ("uwowi", "ươi", "ơ"),
("uwowu", "ươu", "ơ"), ("uaay", "uây", "â"),
("oai", "oai", "a"), ("oay", "oay", "a"), ("oeo", "oeo", "e"),
("uya", "uya", "y"), ("uyu", "uyu", "y"), ("uyee", "uyê", "ê"),
("oai", "oai", "a"), ("oay", "oay", "a"),
("uyee", "uyê", "ê"),
]
for i, o, v in tab:
emit(i, o)
for t in tone:
emit(i+t, o.replace(v, addtone(v, t), 1))
emit("uya", "uya")
def modvowels():
for i, o in modvowel:
@@ -102,6 +94,14 @@ def mod1tone():
for t in tone:
emit(i+t, addtone(o, t))
modvowel = [
("aw", "ă"),
("aa", "â"),
("ee", "ê"),
("oo", "ô"),
("ow", "ơ"),
("uw", "ư"),
]
def tone1mod():
# a+s+w -> ắ
for i, o in modvowel:
@@ -136,11 +136,10 @@ def final():
("aw", "ă", "ă"), ("aa", "â", "â"), ("ee", "ê", "ê"),
("oo", "ô", "ô"), ("ow", "ơ", "ơ"), ("uw", "ư", "ư"),
("iee", "", "ê"), ("yee", "", "ê"), ("uoo", "", "ô"),
("uow", "ươ", "ơ"), ("uwow", "ươ", "ơ"), ("uyee", "uyê", "ê"),
("uaa", "", "â"), ("uee", "", "ê"), ("oaw", "", "ă"),
("uow", "ươ", "ơ"), ("uaa", "", "â"), ("oaw", "", "ă"),
("ai", "ai", "a"), ("ao", "ao", "a"), ("au", "au", "a"),
("ay", "ay", "a"), ("oa", "oa", "a"), ("oe", "oe", "e"),
("oi", "oi", "o"), ("ui", "ui", "u"), ("uy", "uy", "y"),
("ay", "ay", "a"), ("oa", "oa", "a"), ("oi", "oi", "o"),
("ui", "ui", "u"), ("uy", "uy", "y"),
]
for i, o, v in tab:
for c in coda:
@@ -148,24 +147,6 @@ def final():
for t in tone:
emit(i+c+t, o.replace(v, addtone(v, t), 1)+c)
def onsets():
# Keep the qu-/gi- onset in the preedit so the tone lands on the rime
# nucleus, not on the onset glide (qua->quá not qúa, gia->giá not gía).
# transvi (onsetglide) knows to skip the glide; here we only need the
# composed clusters to exist so the preedit accumulates them.
vowels = set("aeiouy")
tones = set("sfrxj")
base = [(i, o) for (i, o) in list(entries)
if i and i[0] in vowels and not (set(i) & tones)]
for i, o in base:
if i[0] != 'u': # no qu+u syllable; u is the glide
emit("qu" + i, "qu" + o)
emit("gi" + i, "gi" + o)
# gi- with the i as nucleus (no following vowel): gì, gìn, gìm, ...
for c in ["", "c", "m", "n", "p", "t", "ch", "ng", "nh"]:
if c:
emit("gi" + c, "gi" + c)
vowel1()
vowel2()
vowel3()
@@ -176,5 +157,3 @@ tone1mod()
tone2mod()
escape()
final()
emit("\\", "\\")
onsets()

File diff suppressed because it is too large Load Diff

28294
map/pinyin.dict Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,69 +1,2 @@
#!/bin/bash
set -euo pipefail
transcode()
{
local file
if (( $# == 0 )); then
iconv -f EUC-JP -t UTF-8
return
fi
for file in "$@"; do
iconv -f EUC-JP -t UTF-8 "$file"
printf '\n'
done
}
transcode "$@" | awk '
function fail(s) {
print "skk2ktrans: " FILENAME ":" FNR ": " s >"/dev/stderr"
bad = 1
exit 1
}
function add(k, v, id) {
sub(/;.*/, "", v)
if(v == "" || v ~ /^[([#]/ || v ~ /[[:space:]]/)
return
id = k SUBSEP v
if(seen[id])
return
seen[id] = 1
if(!(k in row))
order[++nkey] = k
else
row[k] = row[k] " "
row[k] = row[k] v
}
/^;;/ {
if(!body) # the header, with its license notice, is kept
print
next
}
/^[[:space:]]*$/ {
next
}
{
body = 1
if(!match($0, /[[:space:]]+/))
fail("missing candidate list")
key = substr($0, 1, RSTART-1)
field = substr($0, RSTART+RLENGTH)
if(key == "" || key ~ /^;/ || field !~ /^\/.*\/$/)
fail("invalid row")
if(field ~ /\\/)
fail("escaped candidates are unsupported")
if(field ~ /\/[([#]/)
fail("expression candidates are unsupported")
n = split(substr(field, 2, length(field)-2), value, "/")
for(i = 1; i <= n; i++)
add(key, value[i])
}
END {
if(bad)
exit 1
for(i = 1; i <= nkey; i++)
print order[i] "\t" row[order[i]]
}
'
#!/bin/rc
tcs -sf ujis | awk '$1 !~ /;;/ {gsub("(^\/|\/$)", "", $2); gsub(" ", " "); gsub("\/", " ", $2);} {print}'

View File

@@ -1,103 +0,0 @@
# Result first, followed by one or more TAB-separated aliases.
⚠ !!
★ **
± +-
→ ->
· ..
… ...
÷ ./
☹ :(
☺ :)
# Bare 1-9 choose from a prefix search; keep digit aliases in matching slots.
← <- <
≤ <= <
♥ <3 <
≠ <> != <
≡ ==
⇒ =>
≥ >=
¹ ^1 ^
² ^2 ^
³ ^3 ^
⁴ ^4 ^
⁵ ^5 ^
⁶ ^6 ^
⁷ ^7 ^
⁸ ^8 ^
⁹ ^9 ^
₁ _1 _
₂ _2 _
₃ _3 _
₄ _4 _
₅ _5 _
₆ _6 _
₇ _7 _
₈ _8 _
₉ _9 _
⁽ ^(
⁾ ^)
⁺ ^+
⁻ ^-
⁼ ^=
₍ _(
₎ _)
₊ _+
₋ _-
₌ _=
≈ ~=
⁰ ^0
₀ _0
ₐ _a
α alpha
β beta
χ chi
° deg degree
δ delta
Δ De Delta
↓ dn down
ₑ _e
ε eps
η eta
凸 fuck
γ gamma
Γ Ga Gamma
ⁱ ^i
∫ II integral
∞ inf infinity
ι iota
κ kappa
λ lambda
Λ La Lambda
× mul times
μ mu
ⁿ ^n
ν nu
ω omega
Ω Om Omega
● oo circle
ₒ _o
φ phi
Φ Ph Phi
π pi
Π Pi
∏ PP prod
ψ psi
Ψ Ps Psi
ρ rho
σ sigma
Σ Si Sigma
√ sq sqrt
∑ SS sum
τ tau
θ theta
Θ Th Theta
↑ up
υ ups
✓ vv check
ξ xi
Ξ Xi
✗ xx cross
ₓ _x
ζ zeta

File diff suppressed because it is too large Load Diff

View File

@@ -1,73 +0,0 @@
#!/usr/bin/env python3
"""Verify the text maps and dictionaries consumed by strans."""
import argparse
import sys
from pathlib import Path
def verify(path):
errors = []
try:
data = path.read_bytes()
except OSError as error:
return [f"{path}: {error.strerror}"]
try:
text = data.decode("utf-8")
except UnicodeDecodeError as error:
return [f"{path}: invalid UTF-8: {error}"]
keys = {}
for lineno, line in enumerate(text.split("\n"), 1):
where = f"{path}:{lineno}"
if "\r" in line:
errors.append(f"{where}: carriage return is not canonical")
line = line.replace("\r", "")
if not line or line.startswith(";"):
continue
if "\0" in line:
errors.append(f"{where}: embedded NUL")
if line.count("\t") != 1:
errors.append(f"{where}: expected exactly one tab")
continue
key, value = line.split("\t")
if not key:
errors.append(f"{where}: empty key")
if len(key) > 64:
errors.append(f"{where}: key has {len(key)} runes; maximum is 64")
if key in keys:
errors.append(f"{where}: duplicate key; first defined on line {keys[key]}")
else:
keys[key] = lineno
if not value:
errors.append(f"{where}: empty value")
continue
if value != " ".join(value.split(" ")):
errors.append(f"{where}: noncanonical candidate spacing")
values = value.split(" ")
if path.name.endswith(".map"):
if len(value) > 64:
errors.append(f"{where}: value exceeds 64 runes")
else:
for candidate in values:
if len(candidate) > 64:
errors.append(f"{where}: candidate exceeds 64 runes")
if len(values) != len(set(values)):
errors.append(f"{where}: duplicate candidate")
return errors
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("file", nargs="+", type=Path)
args = parser.parse_args()
errors = []
for path in args.file:
errors.extend(verify(path))
for error in errors:
print(error, file=sys.stderr)
return bool(errors)
if __name__ == "__main__":
raise SystemExit(main())

83100
map/wubi.dict Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,257 +0,0 @@
#include "dat.h"
#include "fn.h"
enum {
Asciitofull = 0xFEE0,
};
int popupscale = 1;
static void
fill(u32int *buf, int n, u32int color)
{
int i;
for(i = 0; i < n; i++)
buf[i] = color;
}
static void
fillrect(u32int *buf, int w, int h, int x, int y, int rw, int rh,
u32int color)
{
int x0, x1, y0, y1;
x0 = min(max(x, 0), w);
x1 = min(max(x + max(rw, 0), 0), w);
y0 = min(max(y, 0), h);
y1 = min(max(y + max(rh, 0), 0), h);
for(; y0 < y1; y0++)
fill(buf + y0*w + x0, max(x1 - x0, 0), color);
}
static int
validarea(Area *a)
{
return a != nil && a->w > 0 && a->h > 0;
}
static vlong
areadistance(Area *a, int x, int y)
{
vlong dx, dy, right, bottom;
right = (vlong)a->x + a->w;
bottom = (vlong)a->y + a->h;
dx = x < a->x ? (vlong)a->x - x : x >= right ? x - right : 0;
dy = y < a->y ? (vlong)a->y - y : y >= bottom ? y - bottom : 0;
return dx + dy;
}
void
popuparea(Area *mon, int nmon, Area *work, int x, int y, Area *out)
{
vlong best, bottom, d, right, x0, x1, y0, y1;
int i, pick;
memset(out, 0, sizeof *out);
pick = -1;
best = (vlong)1 << 62;
for(i = 0; i < nmon; i++){
if(!validarea(&mon[i]))
continue;
right = (vlong)mon[i].x + mon[i].w;
bottom = (vlong)mon[i].y + mon[i].h;
if(x >= mon[i].x && x < right &&
y >= mon[i].y && y < bottom){
pick = i;
break;
}
d = areadistance(&mon[i], x, y);
if(d < best){
best = d;
pick = i;
}
}
if(pick < 0)
return;
*out = mon[pick];
if(!validarea(work))
return;
x0 = max((vlong)out->x, work->x);
y0 = max((vlong)out->y, work->y);
x1 = min((vlong)out->x + out->w, (vlong)work->x + work->w);
y1 = min((vlong)out->y + out->h, (vlong)work->y + work->h);
if(x1 <= x0 || y1 <= y0)
return;
out->x = x0;
out->y = y0;
out->w = x1 - x0;
out->h = y1 - y0;
}
int
pagemarker(char *buf, int nbuf, int first, int shown, int total)
{
if(nbuf <= 0)
return 0;
buf[0] = 0;
if(shown <= 0 || (first == 0 && shown >= total))
return 0;
return snprint(buf, nbuf, "%d-%d/%d", first + 1,
first + shown, total);
}
static int
fitrows(int h, int pre, int mark, int n)
{
int fixed;
fixed = 2*PopupPad + mark*Fontsz;
if(pre)
fixed += Fontsz + (n != 0 ? PopupSep : 0);
if(h <= fixed)
return 0;
return min(n, (h - fixed) / Fontsz);
}
void
popuplayout(Drawcmd *dc, int areaw, int areah, Popup *p)
{
char buf[32];
int first, i, markrow, nall, nmark, npre, total, width, y;
memset(p, 0, sizeof *p);
p->prey = -1;
p->sepy = -1;
p->rowsy = -1;
p->marky = -1;
p->sely = -1;
nall = min(max(dc->nkouho, 0), Maxdisp);
npre = dc->pre.n != 0;
if((nall == 0 && !npre) || areaw <= 0 || areah <= 0)
return;
/* As many rows as fit, and a page marker when they do not all. */
total = max(dc->total, dc->first + nall);
markrow = dc->first > 0 || nall < total;
p->n = fitrows(areah, npre, markrow, nall);
if(p->n < nall && !markrow){
markrow = 1;
p->n = fitrows(areah, npre, markrow, nall);
}
if(p->n == 0 && !npre)
return;
if(p->n != 0 && dc->sel >= p->n)
p->row0 = min(dc->sel - p->n + 1, nall - p->n);
first = dc->first + p->row0;
nmark = markrow ? pagemarker(buf, sizeof buf, first, p->n, total) : 0;
if(nmark == 0)
markrow = 0;
/* Rows share one steady width; a preedit alone hugs its text. */
width = p->n != 0 ? PopupBasew : 2*PopupPad;
if(npre)
width = max(width, textwidth(&dc->pre) + 2*PopupPad);
for(i = 0; i < p->n; i++)
width = max(width, textwidth(&dc->kouho[p->row0+i]) +
PopupNumw + 2*PopupPad);
if(markrow){
sinit(&p->mark, buf, nmark);
p->markw = textwidth(&p->mark);
width = max(width, p->markw + 2*PopupPad);
}
p->w = min(width, areaw);
y = PopupPad;
if(npre){
p->prey = y;
y += Fontsz;
}
if(npre && p->n != 0){
p->sepy = y;
y += PopupSep;
}
if(p->n != 0){
p->rowsy = y;
y += p->n*Fontsz;
}
if(markrow){
p->marky = y;
y += Fontsz;
}
p->h = min(y + PopupPad, areah);
p->textw = max(p->w - 2*PopupPad - PopupNumw, 0);
if(markrow){
p->markw = min(p->markw, max(p->w - 2*PopupPad, 0));
p->markx = p->w - PopupPad - p->markw;
}
p->selw = max(p->w - 2*PopupPad, 0);
if(dc->sel >= p->row0 && dc->sel < p->row0 + p->n)
p->sely = p->rowsy + (dc->sel - p->row0)*Fontsz;
}
void
popupdraw(u32int *img, Drawcmd *dc, Popup *p)
{
Str num;
u32int color;
int i, j, y;
fill(img, p->w*p->h, Colbg);
if(p->prey >= 0)
textdraw(img, p->w, p->h, PopupPad, p->prey,
max(p->w - 2*PopupPad, 0), Colfg, &dc->pre);
if(p->sepy >= 0)
fillrect(img, p->w, p->h, PopupPad, p->sepy,
p->w - 2*PopupPad, PopupSep, Colsep);
if(p->sely >= 0)
fillrect(img, p->w, p->h, PopupPad, p->sely,
p->selw, Fontsz, Colsel);
for(i = 0, y = p->rowsy; i < p->n; i++, y += Fontsz){
j = p->row0 + i;
color = j == dc->sel ? Colselfg : Colfg;
sclear(&num);
sputr(&num, '1' + j + Asciitofull);
textdraw(img, p->w, p->h, PopupPad, y,
min(PopupNumw, max(p->w - 2*PopupPad, 0)), color, &num);
textdraw(img, p->w, p->h, PopupPad + PopupNumw, y, p->textw,
color, &dc->kouho[j]);
}
if(p->marky >= 0)
textdraw(img, p->w, p->h, p->markx, p->marky, p->markw,
Colfg, &p->mark);
}
void
popupposition(Caret *caret, int pointerx, int pointery, Area *area,
int w, int h, int *x, int *y)
{
vlong below, bottom, px, py, right, xmax, ymax;
if(!validarea(area)){
*x = 0;
*y = 0;
return;
}
right = (vlong)area->x + area->w;
bottom = (vlong)area->y + area->h;
if(caret->valid){
px = caret->x;
below = (vlong)caret->y + max(caret->h, 0);
if(below >= area->y && below + h <= bottom)
py = below;
else
py = (vlong)caret->y - h;
}else{
px = (vlong)pointerx + 10;
py = (vlong)pointery + 10;
if(py + h > bottom)
py = (vlong)pointery - 10 - h;
}
xmax = max(right - w, area->x);
ymax = max(bottom - h, area->y);
*x = max((vlong)area->x, min(px, xmax));
*y = max((vlong)area->y, min(py, ymax));
}

View File

@@ -1,494 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<protocol name="input_method_unstable_v2">
<copyright>
Copyright © 2008-2011 Kristian Høgsberg
Copyright © 2010-2011 Intel Corporation
Copyright © 2012-2013 Collabora, Ltd.
Copyright © 2012, 2013 Intel Corporation
Copyright © 2015, 2016 Jan Arne Petersen
Copyright © 2017, 2018 Red Hat, Inc.
Copyright © 2018 Purism SPC
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 (including the next
paragraph) 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.
</copyright>
<description summary="Protocol for creating input methods">
This protocol allows applications to act as input methods for compositors.
An input method context is used to manage the state of the input method.
Text strings are UTF-8 encoded, their indices and lengths are in bytes.
This document adheres to the RFC 2119 when using words like "must",
"should", "may", etc.
Warning! The protocol described in this file is experimental and
backward incompatible changes may be made. Backward compatible changes
may be added together with the corresponding interface version bump.
Backward incompatible changes are done by bumping the version number in
the protocol and interface names and resetting the interface version.
Once the protocol is to be declared stable, the 'z' prefix and the
version number in the protocol and interface names are removed and the
interface version number is reset.
</description>
<interface name="zwp_input_method_v2" version="1">
<description summary="input method">
An input method object allows for clients to compose text.
The objects connects the client to a text input in an application, and
lets the client to serve as an input method for a seat.
The zwp_input_method_v2 object can occupy two distinct states: active and
inactive. In the active state, the object is associated to and
communicates with a text input. In the inactive state, there is no
associated text input, and the only communication is with the compositor.
Initially, the input method is in the inactive state.
Requests issued in the inactive state must be accepted by the compositor.
Because of the serial mechanism, and the state reset on activate event,
they will not have any effect on the state of the next text input.
There must be no more than one input method object per seat.
</description>
<enum name="error">
<entry name="role" value="0" summary="wl_surface has another role"/>
</enum>
<event name="activate">
<description summary="input method has been requested">
Notification that a text input focused on this seat requested the input
method to be activated.
This event serves the purpose of providing the compositor with an
active input method.
This event resets all state associated with previous
surrounding_text, text_change_cause, and content_type events, as well
as the state associated with set_preedit_string, commit_string, and
delete_surrounding_text requests. In addition, it marks the
zwp_input_method_v2 object as active, and makes any existing
zwp_input_popup_surface_v2 objects visible.
The surrounding_text, and content_type events must follow before the
next done event if the text input supports the respective
functionality.
State set with this event is double-buffered. It will get applied on
the next zwp_input_method_v2.done event, and stay valid until changed.
</description>
</event>
<event name="deactivate">
<description summary="deactivate event">
Notification that no focused text input currently needs an active
input method on this seat.
This event marks the zwp_input_method_v2 object as inactive. The
compositor must make all existing zwp_input_popup_surface_v2 objects
invisible until the next activate event.
State set with this event is double-buffered. It will get applied on
the next zwp_input_method_v2.done event, and stay valid until changed.
</description>
</event>
<event name="surrounding_text">
<description summary="surrounding text event">
Updates the surrounding plain text around the cursor, excluding the
preedit text.
If any preedit text is present, it is replaced with the cursor for the
purpose of this event.
The argument text is a buffer containing the surrounding text, and must
include the cursor position, and the complete selection. It should
contain additional characters before and after these. There is a
maximum length of wayland messages, so text can not be longer than 4000
bytes.
cursor is the byte offset of the cursor within the text buffer.
anchor is the byte offset of the selection anchor within the text
buffer. If there is no selected text, anchor must be the same as
cursor.
If this event does not arrive before the first done event, the input
method may assume that the text input does not support this
functionality and ignore following surrounding_text events.
Values set with this event are double-buffered. They will get applied
and set to initial values on the next zwp_input_method_v2.done
event.
The initial state for affected fields is empty, meaning that the text
input does not support sending surrounding text. If the empty values
get applied, subsequent attempts to change them may have no effect.
</description>
<arg name="text" type="string"/>
<arg name="cursor" type="uint"/>
<arg name="anchor" type="uint"/>
</event>
<event name="text_change_cause">
<description summary="indicates the cause of surrounding text change">
Tells the input method why the text surrounding the cursor changed.
Whenever the client detects an external change in text, cursor, or
anchor position, it must issue this request to the compositor. This
request is intended to give the input method a chance to update the
preedit text in an appropriate way, e.g. by removing it when the user
starts typing with a keyboard.
cause describes the source of the change.
The value set with this event is double-buffered. It will get applied
and set to its initial value on the next zwp_input_method_v2.done
event.
The initial value of cause is input_method.
</description>
<arg name="cause" type="uint" enum="zwp_text_input_v3.change_cause"/>
</event>
<event name="content_type">
<description summary="content purpose and hint">
Indicates the content type and hint for the current
zwp_input_method_v2 instance.
Values set with this event are double-buffered. They will get applied
on the next zwp_input_method_v2.done event.
The initial value for hint is none, and the initial value for purpose
is normal.
</description>
<arg name="hint" type="uint" enum="zwp_text_input_v3.content_hint"/>
<arg name="purpose" type="uint" enum="zwp_text_input_v3.content_purpose"/>
</event>
<event name="done">
<description summary="apply state">
Atomically applies state changes recently sent to the client.
The done event establishes and updates the state of the client, and
must be issued after any changes to apply them.
Text input state (content purpose, content hint, surrounding text, and
change cause) is conceptually double-buffered within an input method
context.
Events modify the pending state, as opposed to the current state in use
by the input method. A done event atomically applies all pending state,
replacing the current state. After done, the new pending state is as
documented for each related request.
Events must be applied in the order of arrival.
Neither current nor pending state are modified unless noted otherwise.
</description>
</event>
<request name="commit_string">
<description summary="commit string">
Send the commit string text for insertion to the application.
Inserts a string at current cursor position (see commit event
sequence). The string to commit could be either just a single character
after a key press or the result of some composing.
The argument text is a buffer containing the string to insert. There is
a maximum length of wayland messages, so text can not be longer than
4000 bytes.
Values set with this event are double-buffered. They must be applied
and reset to initial on the next zwp_text_input_v3.commit request.
The initial value of text is an empty string.
</description>
<arg name="text" type="string"/>
</request>
<request name="set_preedit_string">
<description summary="pre-edit string">
Send the pre-edit string text to the application text input.
Place a new composing text (pre-edit) at the current cursor position.
Any previously set composing text must be removed. Any previously
existing selected text must be removed. The cursor is moved to a new
position within the preedit string.
The argument text is a buffer containing the preedit string. There is
a maximum length of wayland messages, so text can not be longer than
4000 bytes.
The arguments cursor_begin and cursor_end are counted in bytes relative
to the beginning of the submitted string buffer. Cursor should be
hidden by the text input when both are equal to -1.
cursor_begin indicates the beginning of the cursor. cursor_end
indicates the end of the cursor. It may be equal or different than
cursor_begin.
Values set with this event are double-buffered. They must be applied on
the next zwp_input_method_v2.commit event.
The initial value of text is an empty string. The initial value of
cursor_begin, and cursor_end are both 0.
</description>
<arg name="text" type="string"/>
<arg name="cursor_begin" type="int"/>
<arg name="cursor_end" type="int"/>
</request>
<request name="delete_surrounding_text">
<description summary="delete text">
Remove the surrounding text.
before_length and after_length are the number of bytes before and after
the current cursor index (excluding the preedit text) to delete.
If any preedit text is present, it is replaced with the cursor for the
purpose of this event. In effect before_length is counted from the
beginning of preedit text, and after_length from its end (see commit
event sequence).
Values set with this event are double-buffered. They must be applied
and reset to initial on the next zwp_input_method_v2.commit request.
The initial values of both before_length and after_length are 0.
</description>
<arg name="before_length" type="uint"/>
<arg name="after_length" type="uint"/>
</request>
<request name="commit">
<description summary="apply state">
Apply state changes from commit_string, set_preedit_string and
delete_surrounding_text requests.
The state relating to these events is double-buffered, and each one
modifies the pending state. This request replaces the current state
with the pending state.
The connected text input is expected to proceed by evaluating the
changes in the following order:
1. Replace existing preedit string with the cursor.
2. Delete requested surrounding text.
3. Insert commit string with the cursor at its end.
4. Calculate surrounding text to send.
5. Insert new preedit text in cursor position.
6. Place cursor inside preedit text.
The serial number reflects the last state of the zwp_input_method_v2
object known to the client. The value of the serial argument must be
equal to the number of done events already issued by that object. When
the compositor receives a commit request with a serial different than
the number of past done events, it must proceed as normal, except it
should not change the current state of the zwp_input_method_v2 object.
</description>
<arg name="serial" type="uint"/>
</request>
<request name="get_input_popup_surface">
<description summary="create popup surface">
Creates a new zwp_input_popup_surface_v2 object wrapping a given
surface.
The surface gets assigned the "input_popup" role. If the surface
already has an assigned role, the compositor must issue a protocol
error.
</description>
<arg name="id" type="new_id" interface="zwp_input_popup_surface_v2"/>
<arg name="surface" type="object" interface="wl_surface"/>
</request>
<request name="grab_keyboard">
<description summary="grab hardware keyboard">
Allow an input method to receive hardware keyboard input and process
key events to generate text events (with pre-edit) over the wire. This
allows input methods which compose multiple key events for inputting
text like it is done for CJK languages.
The compositor should send all keyboard events on the seat to the grab
holder via the returned wl_keyboard object. Nevertheless, the
compositor may decide not to forward any particular event. The
compositor must not further process any event after it has been
forwarded to the grab holder.
Releasing the resulting wl_keyboard object releases the grab.
</description>
<arg name="keyboard" type="new_id"
interface="zwp_input_method_keyboard_grab_v2"/>
</request>
<event name="unavailable">
<description summary="input method unavailable">
The input method ceased to be available.
The compositor must issue this event as the only event on the object if
there was another input_method object associated with the same seat at
the time of its creation.
The compositor must issue this request when the object is no longer
usable, e.g. due to seat removal.
The input method context becomes inert and should be destroyed after
deactivation is handled. Any further requests and events except for the
destroy request must be ignored.
</description>
</event>
<request name="destroy" type="destructor">
<description summary="destroy the text input">
Destroys the zwp_text_input_v2 object and any associated child
objects, i.e. zwp_input_popup_surface_v2 and
zwp_input_method_keyboard_grab_v2.
</description>
</request>
</interface>
<interface name="zwp_input_popup_surface_v2" version="1">
<description summary="popup surface">
This interface marks a surface as a popup for interacting with an input
method.
The compositor should place it near the active text input area. It must
be visible if and only if the input method is in the active state.
The client must not destroy the underlying wl_surface while the
zwp_input_popup_surface_v2 object exists.
</description>
<event name="text_input_rectangle">
<description summary="set text input area position">
Notify about the position of the area of the text input expressed as a
rectangle in surface local coordinates.
This is a hint to the input method telling it the relative position of
the text being entered.
</description>
<arg name="x" type="int"/>
<arg name="y" type="int"/>
<arg name="width" type="int"/>
<arg name="height" type="int"/>
</event>
<request name="destroy" type="destructor"/>
</interface>
<interface name="zwp_input_method_keyboard_grab_v2" version="1">
<!-- Closely follows wl_keyboard version 6 -->
<description summary="keyboard grab">
The zwp_input_method_keyboard_grab_v2 interface represents an exclusive
grab of the wl_keyboard interface associated with the seat.
</description>
<event name="keymap">
<description summary="keyboard mapping">
This event provides a file descriptor to the client which can be
memory-mapped to provide a keyboard mapping description.
</description>
<arg name="format" type="uint" enum="wl_keyboard.keymap_format"
summary="keymap format"/>
<arg name="fd" type="fd" summary="keymap file descriptor"/>
<arg name="size" type="uint" summary="keymap size, in bytes"/>
</event>
<event name="key">
<description summary="key event">
A key was pressed or released.
The time argument is a timestamp with millisecond granularity, with an
undefined base.
</description>
<arg name="serial" type="uint" summary="serial number of the key event"/>
<arg name="time" type="uint" summary="timestamp with millisecond granularity"/>
<arg name="key" type="uint" summary="key that produced the event"/>
<arg name="state" type="uint" enum="wl_keyboard.key_state"
summary="physical state of the key"/>
</event>
<event name="modifiers">
<description summary="modifier and group state">
Notifies clients that the modifier and/or group state has changed, and
it should update its local state.
</description>
<arg name="serial" type="uint" summary="serial number of the modifiers event"/>
<arg name="mods_depressed" type="uint" summary="depressed modifiers"/>
<arg name="mods_latched" type="uint" summary="latched modifiers"/>
<arg name="mods_locked" type="uint" summary="locked modifiers"/>
<arg name="group" type="uint" summary="keyboard layout"/>
</event>
<request name="release" type="destructor">
<description summary="release the grab object"/>
</request>
<event name="repeat_info">
<description summary="repeat rate and delay">
Informs the client about the keyboard's repeat rate and delay.
This event is sent as soon as the zwp_input_method_keyboard_grab_v2
object has been created, and is guaranteed to be received by the
client before any key press event.
Negative values for either rate or delay are illegal. A rate of zero
will disable any repeating (regardless of the value of delay).
This event can be sent later on as well with a new value if necessary,
so clients should continue listening for the event past the creation
of zwp_input_method_keyboard_grab_v2.
</description>
<arg name="rate" type="int"
summary="the rate of repeating keys in characters per second"/>
<arg name="delay" type="int"
summary="delay in milliseconds since key down until repeating starts"/>
</event>
</interface>
<interface name="zwp_input_method_manager_v2" version="1">
<description summary="input method manager">
The input method manager allows the client to become the input method on
a chosen seat.
No more than one input method must be associated with any seat at any
given time.
</description>
<request name="get_input_method">
<description summary="request an input method object">
Request a new input zwp_input_method_v2 object associated with a given
seat.
</description>
<arg name="seat" type="object" interface="wl_seat"/>
<arg name="input_method" type="new_id" interface="zwp_input_method_v2"/>
</request>
<request name="destroy" type="destructor">
<description summary="destroy the input method manager">
Destroys the zwp_input_method_manager_v2 object.
The zwp_input_method_v2 objects originating from it remain valid.
</description>
</request>
</interface>
</protocol>

View File

@@ -1,112 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<protocol name="virtual_keyboard_unstable_v1">
<copyright>
Copyright © 2008-2011 Kristian Høgsberg
Copyright © 2010-2013 Intel Corporation
Copyright © 2012-2013 Collabora, Ltd.
Copyright © 2018 Purism SPC
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 (including the next
paragraph) 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.
</copyright>
<interface name="zwp_virtual_keyboard_v1" version="1">
<description summary="virtual keyboard">
The virtual keyboard provides an application with requests which emulate
the behaviour of a physical keyboard.
This interface can be used by clients on its own to provide raw input
events, or it can accompany the input method protocol.
</description>
<request name="keymap">
<description summary="keyboard mapping">
Provide a file descriptor to the compositor which can be
memory-mapped to provide a keyboard mapping description.
</description>
<arg name="format" type="uint" enum="wl_keyboard.keymap_format" summary="keymap format"/>
<arg name="fd" type="fd" summary="keymap file descriptor"/>
<arg name="size" type="uint" summary="keymap size, in bytes"/>
</request>
<enum name="error">
<entry name="no_keymap" value="0" summary="No keymap was set"/>
<entry name="invalid_keymap_format" value="1" summary="Invalid keymap format"/>
</enum>
<request name="key">
<description summary="key event">
A key was pressed or released.
The time argument is a timestamp with millisecond granularity, with an
undefined base. All requests regarding a single object must share the
same clock.
Keymap must be set before issuing this request.
State carries a value from the key_state enumeration.
</description>
<arg name="time" type="uint" summary="timestamp with millisecond granularity"/>
<arg name="key" type="uint" summary="key that produced the event"/>
<arg name="state" type="uint" summary="physical state of the key"/>
</request>
<request name="modifiers">
<description summary="modifier and group state">
Notifies the compositor that the modifier and/or group state has
changed, and it should update state.
The client should use wl_keyboard.modifiers event to synchronize its
internal state with seat state.
Keymap must be set before issuing this request.
</description>
<arg name="mods_depressed" type="uint" summary="depressed modifiers"/>
<arg name="mods_latched" type="uint" summary="latched modifiers"/>
<arg name="mods_locked" type="uint" summary="locked modifiers"/>
<arg name="group" type="uint" summary="keyboard layout"/>
</request>
<request name="destroy" type="destructor" since="1">
<description summary="destroy the virtual keyboard keyboard object"/>
</request>
</interface>
<interface name="zwp_virtual_keyboard_manager_v1" version="1">
<description summary="virtual keyboard manager">
A virtual keyboard manager allows an application to provide keyboard
input events as if they came from a physical keyboard.
</description>
<enum name="error">
<entry name="unauthorized" value="0" summary="client not authorized to use the interface"/>
</enum>
<request name="create_virtual_keyboard">
<description summary="Create a new virtual keyboard">
Creates a new virtual keyboard associated to a seat.
If the compositor enables a keyboard to perform arbitrary actions, it
should present an error when an untrusted client requests a new
keyboard.
</description>
<arg name="seat" type="object" interface="wl_seat"/>
<arg name="id" type="new_id" interface="zwp_virtual_keyboard_v1"/>
</request>
</interface>
</protocol>

27
run.sh
View File

@@ -1,24 +1,9 @@
#!/bin/sh
cd "$(dirname "$0")" || exit 1
pkill strans
pkill strans-xim
if test "$#" -ne 0; then
echo "usage: run.sh" >&2
exit 1
fi
if ! test -x ./strans; then
echo "run.sh: ./strans is not executable; run make first" >&2
exit 1
fi
if ! command -v pkill >/dev/null 2>&1; then
echo "run.sh: pkill is required" >&2
exit 1
fi
uid=$(id -u) || exit 1
pkill -TERM -u "$uid" -x strans 2>/dev/null || :
sleep 0.1
pkill -KILL -u "$uid" -x strans 2>/dev/null || :
./strans map </dev/null &
sleep 1
./strans map font &
sleep 1
xim/strans-xim &

149
srv.c
View File

@@ -1,150 +1,41 @@
#include "dat.h"
#include "fn.h"
#include <sys/stat.h>
#include <sys/un.h>
static char adir[40];
static char adir[256];
static char sockpath[sizeof(((struct sockaddr_un*)0)->sun_path)];
static dev_t sockdev;
static ino_t sockino;
static Channel *clientc;
/* Removes the socket only while it is still the one we announced. */
static void
srvunlink(void)
sendkey(int fd, u32int ks, u32int mod)
{
struct stat st;
Keyreq kr;
if(sockpath[0] != '\0' && lstat(sockpath, &st) == 0 &&
st.st_dev == sockdev && st.st_ino == sockino)
unlink(sockpath);
sockpath[0] = '\0';
kr.fd = fd;
kr.ks = ks;
kr.mod = mod;
chansend(keyc, &kr);
}
static int
srvnote(void *v, char *note)
{
USED(v);
if(notefatal(note))
srvunlink();
return 0;
}
/*
* 0 for a request to make, 1 for a frame that only changes what rides
* along with the requests after it, -1 for anything else.
*/
static int
srvreadreq(int fd, Keyreq *kr, int *want)
{
uchar req[Ipccaretsz];
char text[Ipcfieldmax];
int valid;
size_t n;
int32_t x, y, h;
if(ipcreadn(fd, req, Ipcreqsz) < 0)
return -1;
*want = 0;
kr->ks = 0;
kr->mod = 0;
switch(ipcreqtype(req)){
case Ipckey:
ipcunpackreq(req, want, &kr->mod, &kr->ks);
kr->op = ipcreqreset(req) ? Keyreset : Keypress;
return 0;
case Ipccap:
*want = (req[0] & Ipcreqwant) != 0;
kr->op = Keycap;
return 0;
case Ipccaret:
if(ipcreadn(fd, req + Ipcreqsz, Ipccaretsz - Ipcreqsz) < 0 ||
ipcunpackcaret(req, &valid, &x, &y, &h) < 0)
return -1;
kr->op = Keycaret;
kr->caret.valid = valid;
kr->caret.x = x;
kr->caret.y = y;
kr->caret.h = h;
return 0;
case Ipcsurround:
n = ipcsurroundlen(req);
if(n > 0 && ipcreadn(fd, text, n) < 0)
return -1;
stail(&kr->surround, text, n);
return 1;
}
return -1;
}
/*
* One Keyreq persists for the connection: the negotiated preedit and
* the last caret ride along with every request, and the socket closing
* releases the engine.
*/
static void
clientthread(void *arg)
{
Keyreq kr;
Keyres res;
uchar out[Ipcmaxresp], token;
char commit[Maxutf], preedit[Maxutf];
int fd, n, ncommit, npreedit, rv, want;
int fd;
uchar req[4];
fd = (int)(uintptr)arg;
threadsetname("client %d", fd);
memset(&kr, 0, sizeof kr);
kr.reply = chancreate(sizeof(Keyres), 0);
kr.owner = &fd;
kr.clientpre = 1;
while((rv = srvreadreq(fd, &kr, &want)) >= 0){
if(rv != 0)
continue;
if(kr.op != Keycaret)
kr.clientpre = want;
chansend(keyc, &kr);
chanrecv(kr.reply, &res);
if(kr.op == Keycaret)
continue;
ncommit = stoutf(&res.commit, commit, sizeof commit);
npreedit = stoutf(&res.preedit, preedit, sizeof preedit);
/* A capability reply is always eaten: it marks the extension. */
n = ipcpackresp(out, sizeof out, kr.op == Keycap || res.eaten,
res.del, commit, ncommit, preedit, npreedit, want);
if(n < 0 || ipcsend(fd, out, n) < 0)
break;
}
kr.op = Keyrelease;
kr.ks = 0;
kr.mod = 0;
chansend(keyc, &kr);
chanrecv(kr.reply, &res);
chanfree(kr.reply);
while(read(fd, req, 4) == 4)
sendkey(fd, req[2] | (req[3] << 8), req[1]);
close(fd);
chanrecv(clientc, &token);
}
void
static void
srvinit(void)
{
struct stat st;
char *addr;
char addr[64];
if(ipcpath(sockpath, sizeof sockpath) < 0)
die("IPC path is too long");
addr = smprint("unix!%s", sockpath);
if(addr == nil)
die("out of memory");
snprint(addr, sizeof(addr), "unix!/tmp/strans.%d", getuid());
remove(addr + 5);
if(announce(addr, adir) < 0)
die("IPC endpoint is already in use: %r");
free(addr);
if(chmod(sockpath, 0600) < 0 || lstat(sockpath, &st) < 0)
die("can't protect IPC endpoint: %s", sockpath);
sockdev = st.st_dev;
sockino = st.st_ino;
atexit(srvunlink);
threadnotify(srvnote, 1);
die("announce: %r");
}
void
@@ -152,19 +43,13 @@ srvthread(void*)
{
char ldir[40];
int fd;
uchar token;
threadsetname("srv");
token = 0;
clientc = chancreate(sizeof token, Maxclients);
srvinit();
for(;;){
fd = listen(adir, ldir);
if(fd < 0)
continue;
if(channbsend(clientc, &token) <= 0){
close(fd);
continue;
}
proccreate(clientthread, (void*)(uintptr)fd, 8192);
}
}

5079
stb_truetype.h Normal file

File diff suppressed because it is too large Load Diff

54
str.c
View File

@@ -1,42 +1,23 @@
#include "dat.h"
#include "fn.h"
/* Fills s from n bytes of UTF-8; whole, valid, and at most Maxrunes.
* Anything else leaves s empty. */
int
enum
{
Maxrunes = nelem(((Str*)0)->r),
};
void
sinit(Str *s, char *src, int n)
{
Str tmp = {0};
Rune r;
int len;
s->n = 0;
while(n > 0){
if(tmp.n >= Maxrunes || !fullrune(src, n))
return 0;
len = chartorune(&r, src);
if((r == Runeerror && len == 1) || (r >= 0xd800 && r <= 0xdfff))
return 0;
tmp.r[tmp.n++] = r;
while(n > 0 && s->n < Maxrunes){
len = chartorune(&s->r[s->n], src);
s->n++;
src += len;
n -= len;
}
*s = tmp;
return 1;
}
/* The last runes of n bytes of UTF-8, as many as a Str holds. */
void
stail(Str *s, char *src, int n)
{
Rune r;
char *p;
int nr;
nr = utfnlen(src, n);
for(p = src; nr > Maxrunes; nr--)
p += chartorune(&r, p);
sinit(s, p, n - (p - src));
}
void
@@ -48,9 +29,8 @@ sclear(Str *s)
void
sputr(Str *s, Rune r)
{
/* Str is a capped value; appends at capacity leave it unchanged. */
if(s->n >= Maxrunes)
return;
die("sputr overflow");
s->r[s->n++] = r;
}
@@ -64,10 +44,9 @@ spopr(Str *s)
void
sappend(Str *dst, Str *src)
{
int i, n;
int i;
n = src->n;
for(i = 0; i < n && dst->n < Maxrunes; i++)
for(i = 0; i < src->n && dst->n < Maxrunes; i++)
dst->r[dst->n++] = src->r[i];
}
@@ -84,19 +63,14 @@ scmp(Str *a, Str *b)
return 0;
}
/* UTF-8 of s into buf[sz], NUL-terminated, whole runes only. */
int
stoutf(Str *s, char *buf, int sz)
{
char tmp[UTFmax];
int i, n, len;
n = 0;
for(i = 0; i < s->n; i++){
len = runetochar(tmp, &s->r[i]);
if(len > sz - n - 1)
break;
memmove(buf + n, tmp, len);
for(i = 0; i < s->n && n < sz - UTFmax; i++){
len = runetochar(buf + n, &s->r[i]);
n += len;
}
buf[n] = '\0';

1241
strans.c

File diff suppressed because it is too large Load Diff

View File

@@ -1,150 +0,0 @@
CC = 9c
LD = 9l
HOSTCC = cc
PKG_CONFIG ?= pkg-config
CFLAGS ?= -O2 -g
UNIT_CPPFLAGS = -I..
UNIT_CFLAGS = -Wall -Wextra
HOST_CFLAGS = -std=c99 -Wall -Wextra
TEXT_CFLAGS = $(shell $(PKG_CONFIG) --cflags pangocairo cairo fontconfig)
TEXT_LIBS = $(shell $(PKG_CONFIG) --libs pangocairo cairo fontconfig)
DBUS_CFLAGS = $(shell $(PKG_CONFIG) --cflags dbus-1)
DBUS_LIBS = $(shell $(PKG_CONFIG) --libs dbus-1)
GTK_CFLAGS = $(shell $(PKG_CONFIG) --cflags gtk+-3.0)
GTK_LIBS = $(shell $(PKG_CONFIG) --libs gtk+-3.0)
IBUS_CFLAGS = $(shell $(PKG_CONFIG) --cflags dbus-1 xkbcommon)
IBUS_LIBS = $(shell $(PKG_CONFIG) --libs dbus-1 xkbcommon)
IBUS_CLIENT_CFLAGS = $(shell $(PKG_CONFIG) --cflags ibus-1.0)
IBUS_CLIENT_LIBS = $(shell $(PKG_CONFIG) --libs ibus-1.0)
X11_CFLAGS = $(shell $(PKG_CONFIG) --cflags x11)
X11_LIBS = $(shell $(PKG_CONFIG) --libs x11)
XIM_CFLAGS = $(shell $(PKG_CONFIG) --cflags xcb-imdkit xcb-aux xcb-xkb xkbcommon-x11)
XIM_LIBS = $(shell $(PKG_CONFIG) --libs xcb-imdkit xcb-aux xcb-xkb xkbcommon-x11)
WL_CFLAGS = $(shell $(PKG_CONFIG) --cflags wayland-client xkbcommon)
WL_LIBS = $(shell $(PKG_CONFIG) --libs wayland-client xkbcommon)
UNIT_LDLIBS = -lthread -lbio $(TEXT_LIBS) $(IBUS_LIBS) $(XIM_LIBS) $(WL_LIBS)
PROG = unit_test
STRESS = stress_test
LIVESRC = live.c live.h
LIVEBUSSRC = livebus.c livebus.h
SMOKE = ibus_live_test ibus_client_smoke gtk_live_test xim_live_test \
ipc_live_test
FAULT = daemon_collision_test daemon_failure_test daemon_restart_test
LIVE = $(SMOKE) $(FAULT)
TESTSRC = test_util.c str_test.c trie_test.c \
ko_test.c vi_test.c engine_test.c dict_test.c ipc_test.c \
popup_test.c font_test.c ibus_test.c server_test.c compose_test.c \
xim_adapter_test.c wl_adapter_test.c
TESTOBJ = $(TESTSRC:.c=.o)
# imv2.c and vkv1.c are wayland-scanner's; the parent Makefile writes them.
PARENTSRC = str.c trie.c dict.c ko.c vi.c ipc.c popup_layout.c \
font.c compose.c imv2.c vkv1.c
PARENTOBJ = $(PARENTSRC:%.c=unit_%.o)
COMMONOBJ = $(TESTOBJ) $(PARENTOBJ)
OBJS = unit_test.o stress_test.o $(COMMONOBJ)
all: $(PROG) $(STRESS) $(LIVE)
check test: $(PROG)
./$(PROG) $(UNITARGS)
check-live: $(SMOKE) ../strans ../gtk/im-strans.so
./ibus_live_test ../strans ../map ./ibus_client_smoke
./gtk_live_test ../gtk/im-strans.so
./xim_live_test ../strans ../map
./ipc_live_test ../strans ../map
check-stress: $(STRESS) ibus_live_test ipc_live_test $(FAULT) ../strans
./$(STRESS)
./ibus_live_test --capacity ../strans ../map
./ipc_live_test --capacity ../strans ../map
./daemon_collision_test ../strans ../map
./daemon_failure_test ../strans
./daemon_restart_test ../strans ../map
$(PROG): unit_test.o $(COMMONOBJ)
$(LD) $(LDFLAGS) -o $@ unit_test.o $(COMMONOBJ) $(UNIT_LDLIBS) \
$(LDLIBS)
$(STRESS): stress_test.o $(COMMONOBJ)
$(LD) $(LDFLAGS) -o $@ stress_test.o $(COMMONOBJ) $(UNIT_LDLIBS) \
$(LDLIBS)
stress_test.o: unit_test.c
$(CC) $(CPPFLAGS) $(UNIT_CPPFLAGS) $(UNIT_CFLAGS) $(CFLAGS) -DSTRESS \
-c -o $@ $<
ibus_live_test: ibus_live_test.c $(LIVESRC) $(LIVEBUSSRC) ../ipc.c ../ipc.h
$(HOSTCC) $(CPPFLAGS) -I.. $(DBUS_CFLAGS) $(HOST_CFLAGS) $(CFLAGS) \
$(LDFLAGS) -o $@ ibus_live_test.c live.c livebus.c ../ipc.c \
$(DBUS_LIBS) $(LDLIBS)
ibus_client_smoke: ibus_client_smoke.c
$(HOSTCC) $(CPPFLAGS) $(IBUS_CLIENT_CFLAGS) $(HOST_CFLAGS) $(CFLAGS) \
$(LDFLAGS) -o $@ $< $(IBUS_CLIENT_LIBS) $(LDLIBS)
gtk_live_test: gtk_live_test.c $(LIVESRC) ../ipc.c ../ipc.h
$(HOSTCC) $(CPPFLAGS) -I.. $(GTK_CFLAGS) $(HOST_CFLAGS) $(CFLAGS) \
$(LDFLAGS) -o $@ gtk_live_test.c live.c ../ipc.c $(GTK_LIBS) \
-pthread $(LDLIBS)
# The parent make knows what these are built from; always ask it.
../gtk/im-strans.so:
$(MAKE) -C ../gtk
../strans:
$(MAKE) -C .. strans
../imv2.c ../imv2.h ../vkv1.c ../vkv1.h:
$(MAKE) -C .. $(@F)
xim_live_test: xim_live_test.c $(LIVESRC) ../ipc.c ../ipc.h
$(HOSTCC) $(CPPFLAGS) -I.. $(X11_CFLAGS) $(HOST_CFLAGS) $(CFLAGS) \
$(LDFLAGS) -o $@ xim_live_test.c live.c ../ipc.c $(X11_LIBS) \
$(LDLIBS)
ipc_live_test: ipc_live_test.c $(LIVESRC) ../ipc.c ../ipc.h
$(HOSTCC) $(CPPFLAGS) -I.. $(HOST_CFLAGS) $(CFLAGS) $(LDFLAGS) \
-o $@ ipc_live_test.c live.c ../ipc.c $(LDLIBS)
daemon_collision_test: daemon_collision_test.c $(LIVESRC) $(LIVEBUSSRC) \
../ipc.c ../ipc.h
$(HOSTCC) $(CPPFLAGS) -I.. $(DBUS_CFLAGS) $(HOST_CFLAGS) $(CFLAGS) \
$(LDFLAGS) -o $@ daemon_collision_test.c live.c livebus.c \
../ipc.c $(DBUS_LIBS) $(LDLIBS)
daemon_failure_test: daemon_failure_test.c $(LIVESRC) ../ipc.c ../ipc.h
$(HOSTCC) $(CPPFLAGS) -I.. $(HOST_CFLAGS) $(CFLAGS) $(LDFLAGS) -o $@ \
daemon_failure_test.c live.c ../ipc.c $(LDLIBS)
daemon_restart_test: daemon_restart_test.c $(LIVESRC) $(LIVEBUSSRC) \
../ipc.c ../ipc.h
$(HOSTCC) $(CPPFLAGS) -I.. $(DBUS_CFLAGS) $(HOST_CFLAGS) $(CFLAGS) \
$(LDFLAGS) -o $@ daemon_restart_test.c live.c livebus.c ../ipc.c \
$(DBUS_LIBS) $(LDLIBS)
unit_test.o stress_test.o $(TESTOBJ): test.h ../dat.h ../fn.h ../ipc.h \
../cutest/cutest.h
engine_test.o: ../strans.c
ibus_test.o: UNIT_CPPFLAGS += $(IBUS_CFLAGS)
ibus_test.o: ../ibus.c
xim_adapter_test.o: UNIT_CPPFLAGS += $(XIM_CFLAGS)
xim_adapter_test.o: ../xim.c
wl_adapter_test.o unit_imv2.o unit_vkv1.o: UNIT_CPPFLAGS += $(WL_CFLAGS)
wl_adapter_test.o: ../wl.c ../imv2.h ../vkv1.h
server_test.o: ../srv.c
unit_font.o: UNIT_CPPFLAGS += $(TEXT_CFLAGS)
unit_compose.o compose_test.o: UNIT_CPPFLAGS += $(IBUS_CFLAGS)
unit_%.o: ../%.c ../dat.h ../fn.h ../ipc.h
$(CC) $(CPPFLAGS) $(UNIT_CPPFLAGS) $(UNIT_CFLAGS) $(CFLAGS) -c -o $@ $<
%.o: %.c
$(CC) $(CPPFLAGS) $(UNIT_CPPFLAGS) $(UNIT_CFLAGS) $(CFLAGS) -c -o $@ $<
clean:
rm -f $(OBJS) $(PROG) $(STRESS) $(LIVE)
.PHONY: all check check-live check-stress test clean \
../strans ../gtk/im-strans.so

View File

@@ -1,56 +0,0 @@
#include "dat.h"
#include "fn.h"
#include "test.h"
#include <xkbcommon/xkbcommon-keysyms.h>
/*
* A sequence swallows its keys and hands its text back at the end, and
* belongs to the one context, in the one frontend, that started it.
*/
void
compose_sequences(struct ct *t)
{
static int a, b; /* two input contexts, told apart by address */
static const struct {
int who;
void *owner;
u32int sym;
int composing;
char *text;
} keys[] = {
{ Composexim, &a, XKB_KEY_a, 0, "" },
{ Composexim, &a, XKB_KEY_dead_acute, 1, "" },
{ Composexim, &a, XKB_KEY_e, 0, "é" },
{ Composexim, &a, XKB_KEY_Multi_key, 1, "" },
{ Composexim, &a, XKB_KEY_o, 1, "" },
{ Composexim, &a, XKB_KEY_c, 0, "©" },
{ Composexim, &a, XKB_KEY_dead_acute, 1, "" },
{ Composexim, &a, XKB_KEY_Escape, 1, "" },
{ Composexim, &a, XKB_KEY_a, 0, "" },
/* another context of the same frontend starts over */
{ Composexim, &a, XKB_KEY_dead_acute, 1, "" },
{ Composexim, &b, XKB_KEY_e, 0, "" },
/* another frontend does not touch this one's sequence */
{ Composexim, &a, XKB_KEY_dead_acute, 1, "" },
{ Composewl, &a, XKB_KEY_o, 0, "" },
{ Composexim, &a, XKB_KEY_e, 0, "é" },
};
char buf[Maxutf];
int i;
if(!CT_CHECK(t, setenv("XCOMPOSEFILE", "data/compose", 1) == 0))
return;
composeinit();
for(i = 0; i < nelem(keys); i++){
CT_EQ_INT(t, keys[i].composing, composekey(keys[i].who,
keys[i].owner, keys[i].sym, buf, sizeof buf));
CT_EQ_STR(t, keys[i].text, buf);
}
/* a context that goes takes its half-typed sequence with it */
CT_EQ_INT(t, 1,
composekey(Composewl, &a, XKB_KEY_dead_acute, buf, sizeof buf));
composedrop(Composewl, &a);
CT_EQ_INT(t, 0, composekey(Composewl, &a, XKB_KEY_e, buf, sizeof buf));
CT_EQ_STR(t, "", buf);
unsetenv("XCOMPOSEFILE");
}

View File

@@ -1,214 +0,0 @@
#define _GNU_SOURCE
#include <errno.h>
#include <poll.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include "live.h"
#include "livebus.h"
typedef struct Test Test;
struct Test
{
Live l;
Daemon first;
Daemon second;
int ipcfirst;
int ipcnew;
DBusConnection *busfirst;
DBusConnection *busnew;
char address[512];
};
static int
setup(Test *t)
{
memset(t, 0, sizeof *t);
daemoninit(&t->first, "first daemon");
daemoninit(&t->second, "second daemon");
t->ipcfirst = -1;
t->ipcnew = -1;
return livesetup(&t->l, "collision");
}
static int
addressunchanged(Test *t)
{
char contents[2048], address[512];
size_t ncontents;
pid_t declared;
if(!readfile(t->l.addrfile, contents, sizeof contents, &ncontents) ||
!parseaddress(contents, ncontents, address, sizeof address, &declared))
return 0;
if(declared != t->first.pid)
return fail("IBus address PID %ld, expected %ld", (long)declared,
(long)t->first.pid);
return strcmp(address, t->address) == 0 ||
fail("first private IBus address changed after collision");
}
static int
waitsecond(Test *t)
{
struct pollfd pfd;
int n, status, timeout;
int64_t deadline;
deadline = nowms() + Starttimeout;
for(;;){
do
n = waitpid(t->second.pid, &status, WNOHANG);
while(n < 0 && errno == EINTR);
if(n == t->second.pid){
t->second.pid = -1;
readerrors(&t->second);
if(!WIFEXITED(status) || WEXITSTATUS(status) == 0)
return fail("second daemon did not exit unsuccessfully: %#x",
status);
if(strstr(t->second.err, "IPC endpoint is already in use") == NULL)
return fail("second daemon did not report the IPC collision");
return 1;
}
if(n < 0){
fail("waitpid second daemon %ld: %s", (long)t->second.pid,
strerror(errno));
if(errno == ECHILD)
t->second.pid = -1;
return 0;
}
if(!daemonalive(&t->first))
return 0;
timeout = leftms(deadline);
if(timeout == 0){
fail("second daemon did not exit after the endpoint collision");
killdaemon(&t->second, NULL);
return 0;
}
pfd.fd = t->second.errfd;
pfd.events = POLLIN|POLLHUP;
pfd.revents = 0;
n = poll(&pfd, 1, timeout > 20 ? 20 : timeout);
if(n < 0 && errno == EINTR)
continue;
if(n < 0)
return fail("poll second daemon: %s", strerror(errno));
if(pfd.revents != 0)
readerrors(&t->second);
}
}
static int
runcollision(Test *t, char *program, char *mapdir)
{
if(!startdaemon(&t->l, &t->first, program, mapdir) ||
!waitready(&t->l, &t->first, t->address, sizeof t->address))
return 0;
t->ipcfirst = connectsocket(t->l.socket, nowms() + Calltimeout);
if(t->ipcfirst < 0)
return fail("connect persistent IPC client: %s", strerror(errno));
if(!ipcprobe(t->ipcfirst, "persistent pre-collision"))
return 0;
t->busfirst = openbus(t->address, "persistent pre-collision");
if(t->busfirst == NULL ||
!hello(t->busfirst, NULL, 0, "persistent pre-collision Hello"))
return 0;
if(!startdaemon(&t->l, &t->second, program, mapdir) ||
!waitsecond(t) || !daemonalive(&t->first) || !addressunchanged(t))
return 0;
if(!ipcprobe(t->ipcfirst, "persistent post-collision"))
return 0;
if(!createcontext(t->busfirst, NULL, 0,
"persistent post-collision context"))
return 0;
t->ipcnew = connectsocket(t->l.socket, nowms() + Calltimeout);
if(t->ipcnew < 0)
return fail("connect new IPC client after collision: %s",
strerror(errno));
if(!ipcprobe(t->ipcnew, "new post-collision"))
return 0;
t->busnew = openbus(t->address, "new post-collision");
if(t->busnew == NULL ||
!hello(t->busnew, NULL, 0, "new post-collision Hello") ||
!createcontext(t->busnew, NULL, 0, "new post-collision context"))
return 0;
return daemonalive(&t->first);
}
static int
cleanup(Test *t)
{
int n, ok, status;
ok = 1;
closebus(&t->busnew);
closebus(&t->busfirst);
if(t->ipcnew >= 0){
if(close(t->ipcnew) < 0)
ok = fail("close new IPC client: %s", strerror(errno));
t->ipcnew = -1;
}
if(t->ipcfirst >= 0){
if(close(t->ipcfirst) < 0)
ok = fail("close persistent IPC client: %s", strerror(errno));
t->ipcfirst = -1;
}
if(t->second.pid > 0){
do
n = waitpid(t->second.pid, &status, WNOHANG);
while(n < 0 && errno == EINTR);
if(n == t->second.pid)
t->second.pid = -1;
else if(n == 0){
ok = fail("second daemon still running during cleanup");
if(!killdaemon(&t->second, NULL))
ok = 0;
}else{
fail("check second daemon during cleanup: %s", strerror(errno));
ok = 0;
if(errno == ECHILD)
t->second.pid = -1;
else if(!killdaemon(&t->second, NULL))
ok = 0;
}
}
if(!stopdaemon(&t->first))
ok = 0;
if(!closeerrors(&t->first))
ok = 0;
if(!closeerrors(&t->second))
ok = 0;
if(!liveclean(&t->l))
ok = 0;
return ok;
}
int
main(int argc, char **argv)
{
Test test;
int ok;
testname = "daemon_collision_test";
if(argc != 3){
fprintf(stderr, "usage: daemon_collision_test strans mapdir\n");
return 2;
}
ok = setup(&test);
if(ok)
ok = runcollision(&test, argv[1], argv[2]);
if(!cleanup(&test))
ok = 0;
if(!ok){
showerrors(&test.first);
showerrors(&test.second);
return 1;
}
printf("daemon endpoint collision ownership: ok\n");
return 0;
}

View File

@@ -1,138 +0,0 @@
#define _GNU_SOURCE
#include <dirent.h>
#include <errno.h>
#include <poll.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include "live.h"
enum
{
Failtimeout = 4000,
};
static int
waitfailed(Daemon *d, char *badmap)
{
struct pollfd pfd;
int n, status;
int64_t deadline;
status = 0;
deadline = nowms() + Failtimeout;
for(;;){
n = waitpid(d->pid, &status, WNOHANG);
if(n == d->pid){
d->pid = -1;
readerrors(d);
break;
}
if(n < 0 && errno == EINTR)
continue;
if(n < 0){
if(errno == ECHILD)
d->pid = -1;
return fail("waitpid: %s", strerror(errno));
}
n = leftms(deadline);
if(n == 0)
return fail("daemon did not exit after map initialization failure");
pfd.fd = d->errfd;
pfd.events = POLLIN|POLLHUP;
pfd.revents = 0;
n = poll(&pfd, 1, n);
if(n < 0 && errno == EINTR)
continue;
if(n < 0)
return fail("poll daemon: %s", strerror(errno));
if(n > 0)
readerrors(d);
}
if(!WIFEXITED(status) || WEXITSTATUS(status) == 0)
return fail("daemon map failure had wait status %#x", status);
if(strstr(d->err, "can't open") == NULL || strstr(d->err, badmap) == NULL)
return fail("daemon did not report the missing map directory");
return 1;
}
static int
emptydir(char *path)
{
DIR *dir;
struct dirent *de;
int empty;
dir = opendir(path);
if(dir == NULL)
return fail("open %s: %s", path, strerror(errno));
empty = 1;
errno = 0;
while((de = readdir(dir)) != NULL)
if(strcmp(de->d_name, ".") != 0 && strcmp(de->d_name, "..") != 0){
fail("unexpected endpoint %s/%s", path, de->d_name);
empty = 0;
}
if(errno != 0){
fail("read %s: %s", path, strerror(errno));
empty = 0;
}
if(closedir(dir) < 0){
fail("close %s: %s", path, strerror(errno));
empty = 0;
}
return empty;
}
static int
checkendpoints(Live *l)
{
struct stat st;
if(lstat(l->socket, &st) == 0)
return fail("IPC endpoint exists after failed startup: %s", l->socket);
if(errno != ENOENT)
return fail("lstat IPC endpoint: %s", strerror(errno));
return emptydir(l->runtime) && emptydir(l->bus);
}
int
main(int argc, char **argv)
{
Daemon daemon;
Live live;
char badmap[320];
int ok;
testname = "daemon_failure_test";
if(argc != 2){
fprintf(stderr, "usage: daemon_failure_test strans\n");
return 2;
}
daemoninit(&daemon, "daemon");
ok = livesetup(&live, "failure");
if(ok && snprintf(badmap, sizeof badmap, "%s/missing-map", live.root)
>= (int)sizeof badmap)
ok = fail("temporary path is too long");
if(ok)
ok = startdaemon(&live, &daemon, argv[1], badmap);
if(ok)
ok = waitfailed(&daemon, badmap) && checkendpoints(&live);
if(!killdaemon(&daemon, NULL))
ok = 0;
if(!closeerrors(&daemon))
ok = 0;
if(!liveclean(&live))
ok = 0;
if(!ok){
showerrors(&daemon);
return 1;
}
printf("failed daemon startup leaves no endpoints: ok\n");
return 0;
}

View File

@@ -1,406 +0,0 @@
#define _GNU_SOURCE
#include <errno.h>
#include <poll.h>
#include <signal.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include "ipc.h"
#include "live.h"
#include "livebus.h"
enum
{
Ctrlmask = 1<<2,
};
typedef struct Test Test;
struct Test
{
Live l;
Daemon first;
Daemon second;
pid_t firstpid;
int ipcfirst;
int ipcnew;
int busfirstfd;
DBusConnection *busfirst;
DBusConnection *busnew;
char firstaddress[512];
char secondaddress[512];
};
static int
setup(Test *t)
{
struct sigaction sa;
memset(t, 0, sizeof *t);
daemoninit(&t->first, "first daemon");
daemoninit(&t->second, "second daemon");
t->firstpid = -1;
t->ipcfirst = -1;
t->ipcnew = -1;
t->busfirstfd = -1;
memset(&sa, 0, sizeof sa);
sa.sa_handler = SIG_DFL;
if(sigemptyset(&sa.sa_mask) < 0 || sigaction(SIGCHLD, &sa, NULL) < 0)
return fail("establish exact child ownership: %s", strerror(errno));
return livesetup(&t->l, "restart");
}
static int
privateaddress(char *address, pid_t pid)
{
char prefix[96];
size_t n;
if(snprintf(prefix, sizeof prefix, "unix:abstract=strans-%ld",
(long)pid) >= (int)sizeof prefix)
return fail("private IBus prefix is too long");
n = strlen(prefix);
return (strncmp(address, prefix, n) == 0 && address[n] == ',') ||
fail("IBus address is not the daemon's private abstract address");
}
static int
focusin(DBusConnection *conn, char *path)
{
DBusMessage *reply;
reply = callret(conn, contextcall(path, "FocusIn"),
"replacement FocusIn");
if(reply == NULL)
return 0;
if(!dbus_message_has_signature(reply, "")){
dbus_message_unref(reply);
return fail("replacement FocusIn returned a nonempty reply");
}
dbus_message_unref(reply);
return 1;
}
static int
keycall(DBusConnection *conn, char *path, dbus_uint32_t sym,
dbus_uint32_t state, int expected, char *where)
{
DBusMessage *m, *reply;
DBusError err;
dbus_uint32_t code;
dbus_bool_t eaten;
code = 0;
m = contextcall(path, "ProcessKeyEvent");
if(m == NULL || !dbus_message_append_args(m,
DBUS_TYPE_UINT32, &sym, DBUS_TYPE_UINT32, &code,
DBUS_TYPE_UINT32, &state, DBUS_TYPE_INVALID)){
if(m != NULL)
dbus_message_unref(m);
return fail("build %s ProcessKeyEvent call", where);
}
reply = callret(conn, m, where);
if(reply == NULL)
return 0;
dbus_error_init(&err);
if(!dbus_message_has_signature(reply, "b") ||
!dbus_message_get_args(reply, &err, DBUS_TYPE_BOOLEAN, &eaten,
DBUS_TYPE_INVALID)){
dbus_message_unref(reply);
dbus_error_free(&err);
return fail("%s returned an invalid ProcessKeyEvent reply", where);
}
dbus_error_free(&err);
dbus_message_unref(reply);
return (eaten != FALSE) == (expected != 0) ||
fail("%s eaten=%d, expected %d", where, eaten != FALSE, expected);
}
static int
openpersistent(Test *t)
{
t->ipcfirst = connectsocket(t->l.socket, nowms() + Calltimeout);
if(t->ipcfirst < 0)
return fail("connect persistent IPC client: %s", strerror(errno));
if(!ipcprobe(t->ipcfirst, "persistent pre-crash"))
return 0;
t->busfirst = openbus(t->firstaddress, "persistent pre-crash");
if(t->busfirst == NULL ||
!hello(t->busfirst, NULL, 0, "persistent pre-crash Hello") ||
!createcontext(t->busfirst, NULL, 0, "persistent pre-crash context"))
return 0;
if(!dbus_connection_get_unix_fd(t->busfirst, &t->busfirstfd))
return fail("persistent IBus connection has no Unix descriptor");
return 1;
}
/*
* plan9port ignores a broken pipe: the note handlers run and the daemon
* goes on serving, so the endpoints it announced must still be there.
*/
static int
survivenote(Test *t)
{
struct stat st;
int fd, ok;
if(kill(t->firstpid, SIGPIPE) < 0)
return fail("send a broken pipe to the first daemon: %s",
strerror(errno));
pausems(200);
if(!daemonalive(&t->first))
return fail("first daemon died of a broken pipe");
if(lstat(t->l.socket, &st) < 0)
return fail("IPC socket taken by a note the daemon survived: %s",
strerror(errno));
if(lstat(t->l.addrfile, &st) < 0)
return fail("IBus address taken by a note the daemon survived: %s",
strerror(errno));
fd = connectsocket(t->l.socket, nowms() + Calltimeout);
if(fd < 0)
return fail("connect after a broken pipe: %s", strerror(errno));
ok = ipcprobe(fd, "after a broken pipe");
if(close(fd) < 0)
return fail("close the client opened after a broken pipe: %s",
strerror(errno));
return ok;
}
static int
hardcrash(Test *t)
{
int status;
status = 0;
if(!daemonalive(&t->first) || !killdaemon(&t->first, &status))
return 0;
readerrors(&t->first);
if(!WIFSIGNALED(status) || WTERMSIG(status) != SIGKILL)
return fail("first daemon hard crash had wait status %#x", status);
return 1;
}
static int
waitipcclosed(Test *t)
{
struct pollfd pfd;
unsigned char byte;
ssize_t n;
int timeout;
int64_t deadline;
deadline = nowms() + Calltimeout;
for(;;){
timeout = leftms(deadline);
if(timeout == 0)
return fail("persistent IPC connection did not disconnect promptly");
pfd.fd = t->ipcfirst;
pfd.events = POLLIN|POLLHUP|POLLERR;
pfd.revents = 0;
n = poll(&pfd, 1, timeout);
if(n < 0 && errno == EINTR)
continue;
if(n <= 0)
return fail("poll persistent IPC disconnection: %s",
n == 0 ? "timed out" : strerror(errno));
n = recv(t->ipcfirst, &byte, 1, MSG_PEEK);
if(n == 0 || (n < 0 && (errno == ECONNRESET || errno == ENOTCONN ||
errno == EPIPE)))
return 1;
if(n < 0 && (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK))
continue;
if(n < 0)
return fail("read persistent IPC disconnection: %s",
strerror(errno));
return fail("persistent IPC connection retained unread data after crash");
}
}
static int
waitbusclosed(Test *t)
{
struct pollfd pfd;
int n, timeout;
int64_t deadline;
deadline = nowms() + Calltimeout;
for(;;){
if(!dbus_connection_get_is_connected(t->busfirst))
return 1;
timeout = leftms(deadline);
if(timeout == 0)
return fail("persistent IBus connection did not disconnect promptly");
pfd.fd = t->busfirstfd;
pfd.events = POLLIN|POLLHUP|POLLERR;
pfd.revents = 0;
n = poll(&pfd, 1, timeout);
if(n < 0 && errno == EINTR)
continue;
if(n <= 0)
return fail("poll persistent IBus disconnection: %s",
n == 0 ? "timed out" : strerror(errno));
if(pfd.revents & POLLNVAL)
return fail("persistent IBus descriptor became invalid");
dbus_connection_read_write_dispatch(t->busfirst, 0);
}
}
static int
checkstale(Test *t)
{
struct stat st;
char contents[2048], address[512];
size_t ncontents;
pid_t declared;
if(lstat(t->l.socket, &st) < 0)
return fail("stale IPC socket disappeared: %s", strerror(errno));
if(!S_ISSOCK(st.st_mode))
return fail("stale IPC endpoint is not a socket");
if(lstat(t->l.addrfile, &st) < 0)
return fail("stale IBus address file disappeared: %s", strerror(errno));
if(!readfile(t->l.addrfile, contents, sizeof contents, &ncontents) ||
!parseaddress(contents, ncontents, address, sizeof address, &declared))
return 0;
if(declared != t->firstpid || strcmp(address, t->firstaddress) != 0)
return fail("stale IBus address no longer names the dead first daemon");
return 1;
}
/* The dead daemon's abstract socket has no listener, so this must fail. */
static int
rejectold(Test *t, char *where)
{
DBusConnection *conn;
DBusError err;
int ok;
dbus_error_init(&err);
conn = dbus_connection_open_private(t->firstaddress, &err);
if(conn != NULL){
dbus_connection_set_exit_on_disconnect(conn, FALSE);
closebus(&conn);
ok = fail("old private address accepted a connection %s", where);
}else
ok = dbus_error_is_set(&err) ||
fail("old private address failed without a D-Bus error %s", where);
dbus_error_free(&err);
return ok;
}
static int
openreplacement(Test *t)
{
unsigned char selectjp[] = {1, 0, 0, 0, 0, 0};
unsigned char keyk[] = {1, 0, 0, 0, 1, 0, 'k'};
char path[96];
t->ipcnew = connectsocket(t->l.socket, nowms() + Calltimeout);
if(t->ipcnew < 0)
return fail("connect replacement IPC client: %s", strerror(errno));
if(!ipcrequest(t->ipcnew, Mctrl, 'n', selectjp, sizeof selectjp,
"replacement select Japanese") ||
!ipcrequest(t->ipcnew, 0, 'k', keyk, sizeof keyk,
"replacement real key"))
return 0;
t->busnew = openbus(t->secondaddress, "replacement");
if(t->busnew == NULL ||
!hello(t->busnew, NULL, 0, "replacement Hello") ||
!createcontext(t->busnew, path, sizeof path, "replacement context") ||
!focusin(t->busnew, path) ||
!keycall(t->busnew, path, 'n', Ctrlmask, 1,
"replacement select Japanese") ||
!keycall(t->busnew, path, 'k', 0, 1, "replacement real key"))
return 0;
return daemonalive(&t->second);
}
static int
cleanup(Test *t)
{
int ok;
ok = 1;
closebus(&t->busnew);
closebus(&t->busfirst);
if(t->ipcnew >= 0){
if(close(t->ipcnew) < 0)
ok = fail("close replacement IPC client: %s", strerror(errno));
t->ipcnew = -1;
}
if(t->ipcfirst >= 0){
if(close(t->ipcfirst) < 0)
ok = fail("close persistent IPC client: %s", strerror(errno));
t->ipcfirst = -1;
}
if(!killdaemon(&t->first, NULL))
ok = 0;
if(!stopdaemon(&t->second))
ok = 0;
if(!closeerrors(&t->first))
ok = 0;
if(!closeerrors(&t->second))
ok = 0;
if(!liveclean(&t->l))
ok = 0;
return ok;
}
static int
runrestart(Test *t, char *program, char *mapdir)
{
if(!startdaemon(&t->l, &t->first, program, mapdir))
return 0;
t->firstpid = t->first.pid;
if(!waitready(&t->l, &t->first, t->firstaddress, sizeof t->firstaddress) ||
!privateaddress(t->firstaddress, t->firstpid) || !openpersistent(t))
return 0;
if(!survivenote(t))
return 0;
if(!hardcrash(t) || !waitipcclosed(t) || !waitbusclosed(t) ||
!checkstale(t) || !rejectold(t, "after hard crash"))
return 0;
closebus(&t->busfirst);
if(close(t->ipcfirst) < 0)
return fail("close disconnected persistent IPC client: %s",
strerror(errno));
t->ipcfirst = -1;
if(!startdaemon(&t->l, &t->second, program, mapdir) ||
!waitready(&t->l, &t->second, t->secondaddress,
sizeof t->secondaddress) ||
!privateaddress(t->secondaddress, t->second.pid) ||
!openreplacement(t))
return 0;
return daemonalive(&t->second);
}
int
main(int argc, char **argv)
{
Test test;
int ok;
testname = "daemon_restart_test";
if(argc != 3){
fprintf(stderr, "usage: daemon_restart_test strans mapdir\n");
return 2;
}
ok = setup(&test);
if(ok)
ok = runrestart(&test, argv[1], argv[2]);
if(!cleanup(&test))
ok = 0;
if(!ok){
showerrors(&test.first);
showerrors(&test.second);
return 1;
}
printf("daemon endpoints across a note and a hard crash: ok\n");
return 0;
}

View File

@@ -1,2 +0,0 @@
<dead_acute> <e> : "é"
<Multi_key> <o> <c> : "©"

View File

@@ -1,4 +0,0 @@
ka か
sa さ
n ん
na な

View File

@@ -1,4 +0,0 @@
;; converter fixture; the test transcodes this file to EUC-JP
かんじ 漢字 幹事 感じ
えがお 笑顔
きごう 記号 普通
1 ;; converter fixture; the test transcodes this file to EUC-JP
2 かんじ 漢字 幹事 感じ
3 えがお 笑顔
4 きごう 記号 普通

View File

@@ -1,6 +0,0 @@
;; converter fixture; the test transcodes this file to EUC-JP
かんじ /漢字;common/幹事/
えがお /笑顔;face/
かんじ /感じ/漢字;duplicate/
きごう /記号;symbol/普通/
むこう /候補 with space/

View File

@@ -1,5 +0,0 @@
a alpha
ab beta
한 값
duplicate first
duplicate second

View File

@@ -1,88 +0,0 @@
#include "dat.h"
#include "fn.h"
#include "test.h"
void
dictionary_candidates(struct ct *t)
{
char many[8*(Maxkouho+1)], item[8];
char *p;
Str kouho[Maxkouho], key;
Trie *saved;
Lang *lang;
int i, n;
lang = getlang(LangJP);
saved = lang->dict;
lang->dict = trienew();
trieput(lang->dict, "かな", strlen("かな"), " 候補1 候補2 ",
strlen(" 候補1 候補2 "));
key = mkstr("かな");
n = dictlookup(lang->dict, &key, kouho, Maxkouho);
if(CT_EQ_INT(t, 2, n)){
checkstr(t, "candidate 1", "候補1", &kouho[0]);
checkstr(t, "candidate 2", "候補2", &kouho[1]);
}
p = many;
for(i = 0; i < Maxkouho+1; i++){
snprint(item, sizeof item, "c%02d", i);
if(i > 0)
*p++ = ' ';
memmove(p, item, strlen(item));
p += strlen(item);
}
*p = '\0';
trieput(lang->dict, "key", 3, many, strlen(many));
key = mkstr("key");
n = dictlookup(lang->dict, &key, kouho, Maxkouho);
if(CT_EQ_INT(t, Maxkouho, n)){
checkstr(t, "first capped candidate", "c00", &kouho[0]);
snprint(item, sizeof item, "c%02d", Maxkouho-1);
checkstr(t, "last capped candidate", item, &kouho[Maxkouho-1]);
}
CT_EQ_INT(t, 3, dictlookup(lang->dict, &key, kouho, 3));
trieclose(lang->dict);
lang->dict = saved;
}
void
dictionary_misses(struct ct *t)
{
Str kouho[Maxkouho], key;
Trie *saved;
Lang *lang;
lang = getlang(LangJP);
saved = lang->dict;
lang->dict = trienew();
key = mkstr("");
CT_EQ_INT(t, 0, dictlookup(lang->dict, &key, kouho, Maxkouho));
key = mkstr("missing");
CT_EQ_INT(t, 0, dictlookup(lang->dict, &key, kouho, Maxkouho));
CT_EQ_INT(t, 0, dictlookup(getlang(LangKO)->dict, &key, kouho, Maxkouho));
trieclose(lang->dict);
lang->dict = saved;
}
void
dictionary_prefix(struct ct *t)
{
Str kouho[Maxkouho], key;
Trie *dict;
dict = trienew();
trieput(dict, "smile", 5, "A B", 3);
trieput(dict, "smiley", 6, "B C", 3);
trieput(dict, "sad", 3, "D", 1);
key = mkstr("smile");
if(CT_EQ_INT(t, 4, dictprefix(dict, &key, kouho, Maxkouho))){
checkstr(t, "own entry first", "A", &kouho[0]);
checkstr(t, "entry below", "C", &kouho[3]);
}
key = mkstr("s");
CT_EQ_INT(t, 5, dictprefix(dict, &key, kouho, Maxkouho));
CT_EQ_INT(t, 2, dictprefix(dict, &key, kouho, 2));
key = mkstr("x");
CT_EQ_INT(t, 0, dictprefix(dict, &key, kouho, Maxkouho));
trieclose(dict);
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,257 +0,0 @@
#include "dat.h"
#include "fn.h"
#include "test.h"
enum
{
Guard = 8,
Rgbmask = 0xffffff,
};
#define Testw (6 * Fontsz)
#define Testh (3 * Fontsz)
#define Testn (Testw * Testh)
static u32int guardcolor = 0x5a5a5a5a;
static void
fillpixels(u32int *p, int n, u32int color)
{
int i;
for(i = 0; i < n; i++)
p[i] = color;
}
static int
hasink(u32int *p, int n, u32int bg)
{
int i;
bg &= Rgbmask;
for(i = 0; i < n; i++)
if((p[i] & Rgbmask) != bg)
return 1;
return 0;
}
static int
hascolors(u32int *p, int n, u32int bg)
{
u32int c, first;
int b, found, g, i, r;
bg &= Rgbmask;
first = 0;
found = 0;
for(i = 0; i < n; i++){
c = p[i] & Rgbmask;
if(c == bg)
continue;
r = c >> 16 & 0xff;
g = c >> 8 & 0xff;
b = c & 0xff;
if(r == g && g == b)
continue;
if(!found){
first = c;
found = 1;
}else if(c != first)
return 1;
}
return 0;
}
static void
checkguards(struct ct *t, u32int *mem)
{
int i;
for(i = 0; i < Guard; i++){
CT_EQ_UINT(t, guardcolor, mem[i]);
CT_EQ_UINT(t, guardcolor, mem[Guard + Testn + i]);
}
}
static void
checkoutside(struct ct *t, u32int *buf, u32int bg, int row)
{
int x, y, y0, y1;
bg &= Rgbmask;
y0 = max(row, 0);
y1 = min(row + Fontsz, Testh);
for(y = 0; y < Testh; y++){
if(y >= y0 && y < y1)
continue;
for(x = 0; x < Testw; x++)
CT_EQ_UINT(t, bg, buf[y * Testw + x] & Rgbmask);
}
}
static int
inkbounds(u32int *p, u32int bg, int *minx, int *miny, int *maxx, int *maxy)
{
int x, y;
bg &= Rgbmask;
*minx = Testw;
*miny = Testh;
*maxx = -1;
*maxy = -1;
for(y = 0; y < Testh; y++){
for(x = 0; x < Testw; x++){
if((p[y * Testw + x] & Rgbmask) == bg)
continue;
*minx = min(*minx, x);
*miny = min(*miny, y);
*maxx = max(*maxx, x);
*maxy = max(*maxy, y);
}
}
return *maxx >= 0;
}
static void
checktext(struct ct *t, u32int *buf, char *utf)
{
Str s;
int maxx, maxy, minx, miny, w;
s = mkstr(utf);
w = textwidth(&s);
CT_CHECK(t, w > 0);
CT_CHECK(t, w < Testw - Fontsz);
fillpixels(buf, Testn, Colbg);
textdraw(buf, Testw, Testh, Fontsz, Fontsz, Testw, Colfg, &s);
if(!CT_CHECK(t, inkbounds(buf, Colbg, &minx, &miny, &maxx, &maxy)))
return;
CT_CHECK(t, minx >= 0 && miny >= 0);
CT_CHECK(t, maxx < Testw && maxy < Testh);
CT_CHECK(t, maxx - minx + 1 <= w);
}
static void
checkcolor(struct ct *t, u32int *buf, char *utf)
{
Str s;
int w;
s = mkstr(utf);
w = textwidth(&s);
CT_CHECK(t, w > 0);
if(s.n > 1)
CT_CHECK(t, w < s.n * Fontsz);
fillpixels(buf, Testn, Colbg);
textdraw(buf, Testw, Testh, Fontsz, Fontsz, Testw, Colfg, &s);
CT_CHECK(t, hasink(buf, Testn, Colbg));
CT_CHECK(t, hascolors(buf, Testn, Colbg));
}
static void
checkshape(struct ct *t, u32int *buf, char *utf)
{
Str s;
int w;
s = mkstr(utf);
w = textwidth(&s);
CT_CHECK(t, w > 0);
CT_CHECK(t, w < s.n * Fontsz);
fillpixels(buf, Testn, Colbg);
textdraw(buf, Testw, Testh, Fontsz, Fontsz, Testw, Colfg, &s);
CT_CHECK(t, hasink(buf, Testn, Colbg));
}
static void
checkclip(struct ct *t, u32int *mem, int x, int y, Str *s)
{
u32int *buf;
buf = mem + Guard;
fillpixels(buf, Testn, Colbg);
textdraw(buf, Testw, Testh, x, y, Testw, Colfg, s);
CT_CHECK(t, hasink(buf, Testn, Colbg));
checkoutside(t, buf, Colbg, y);
checkguards(t, mem);
}
void
font_render(struct ct *t)
{
static char *plain[] = { "A", "", "", "", "", "𠀋" };
u32int *buf, *mem;
Str a, heart, longrow, missing;
int i, maxx, maxy, minx, miny, natural, w, x, y;
mem = emalloc((Testn + 2 * Guard) * sizeof mem[0]);
fillpixels(mem, Testn + 2 * Guard, guardcolor);
buf = mem + Guard;
fillpixels(buf, Testn, Colbg);
a = mkstr("A");
textinit();
for(i = 0; i < nelem(plain); i++)
checktext(t, buf, plain[i]);
checkcolor(t, buf, "😀");
checkcolor(t, buf, "❤️");
checkcolor(t, buf, "☕️");
checkshape(t, buf, "👩‍💻");
checkshape(t, buf, "👍🏽");
checkshape(t, buf, "🇰🇷");
checkshape(t, buf, "☕︎");
heart = mkstr("❤️");
fillpixels(buf, Testn, Colsel);
textdraw(buf, Testw, Testh, Fontsz, Fontsz, Testw, Colfg, &heart);
CT_CHECK(t, hascolors(buf, Testn, Colsel));
CT_EQ_UINT(t, Colsel,
buf[(Fontsz + Fontsz/2) * Testw + Testw - 1] & Rgbmask);
checkoutside(t, buf, Colsel, Fontsz);
checkguards(t, mem);
w = textwidth(&a);
checkclip(t, mem, -w / 2, Fontsz, &a);
checkclip(t, mem, Testw - w / 2, Fontsz, &a);
checkclip(t, mem, Fontsz, -Fontsz / 2, &a);
checkclip(t, mem, Fontsz, Testh - Fontsz / 2, &a);
longrow = mkstr("abcdefghijklmnopqrstuvwxyz");
natural = textwidth(&longrow);
CT_CHECK(t, natural > 2*Fontsz);
fillpixels(buf, Testn, Colbg);
textdraw(buf, Testw, Testh, 2*Fontsz, Fontsz, 2*Fontsz, Colfg,
&longrow);
CT_CHECK(t, hasink(buf, Testn, Colbg));
for(y = Fontsz; y < 2*Fontsz; y++){
for(x = 0; x < 2*Fontsz; x++)
CT_EQ_UINT(t, Colbg, buf[y * Testw + x] & Rgbmask);
for(x = 4*Fontsz; x < Testw; x++)
CT_EQ_UINT(t, Colbg, buf[y * Testw + x] & Rgbmask);
}
CT_EQ_INT(t, natural, textwidth(&longrow));
fillpixels(buf, Testn, Colsel);
textdraw(buf, Testw, Testh, 2*Fontsz, Fontsz, 2*Fontsz, Colselfg,
&longrow);
CT_CHECK(t, (Colselfg & Rgbmask) != (Colsel & Rgbmask));
if(CT_CHECK(t, inkbounds(buf, Colsel,
&minx, &miny, &maxx, &maxy))){
CT_CHECK(t, minx >= 2*Fontsz);
CT_CHECK(t, maxx < 4*Fontsz);
CT_CHECK(t, miny >= Fontsz);
CT_CHECK(t, maxy < 2*Fontsz);
}
checkoutside(t, buf, Colsel, Fontsz);
checkguards(t, mem);
CT_EQ_INT(t, natural, textwidth(&longrow));
missing = mkstr("\xF4\x8F\xBF\xBF");
for(i = 0; i < 32; i++){
textdraw(buf, Testw, Testh, Fontsz, Fontsz, Testw, Colfg, &heart);
textdraw(buf, Testw, Testh, 0, 0, Testw, Colfg, &missing);
}
checkguards(t, mem);
free(mem);
textclose();
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,386 +0,0 @@
#define _POSIX_C_SOURCE 200809L
#include <ibus.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct Log Log;
struct Log
{
GMainLoop *loop;
int *waiting;
IBusInputContext *a;
IBusInputContext *clearctx;
int legacy;
int modern;
int commit;
int invalid;
int sawpreedit;
int sawcommit;
int done;
int transfer;
int atext;
int aclear;
int retake;
int retook;
char revent[16];
int rn;
int repeat;
int rfirst;
int rcommit;
int rdone;
int flushed;
int hanja;
int required;
int deleted;
int delbefore;
int delcount;
int converted;
};
static void
revent(Log *log, char c)
{
if(log->rn + 1 < (int)sizeof log->revent){
log->revent[log->rn++] = c;
log->revent[log->rn] = '\0';
}
}
static void
legacy(IBusInputContext *ctx, IBusText *text, guint cursor,
gboolean visible, void *arg)
{
IBusAttrList *attrs;
IBusAttribute *a;
Log *log;
const char *s;
(void)ctx;
log = arg;
log->legacy++;
s = ibus_text_get_text(text);
if(log->repeat == 3){
if(s[0] != '\0' || cursor != 0 || visible)
log->invalid = 1;
return;
}
if(log->repeat != 0){
revent(log, 'P');
if(log->repeat == 1){
if(s[0] == '\0'){
if(cursor != 0 || visible)
log->invalid = 1;
}else if(strcmp(s, "") != 0 || cursor != 1 || !visible)
log->invalid = 1;
else
log->rfirst = 1;
}else if(s[0] == '\0'){
if(cursor != 0 || visible || log->rcommit)
log->invalid = 1;
}else if(strcmp(s, "") != 0 || cursor != 1 || !visible ||
!log->rcommit)
log->invalid = 1;
else
log->rdone = 1;
if(log->loop != NULL && log->waiting != NULL && *log->waiting)
g_main_loop_quit(log->loop);
return;
}
if(strcmp(s, "k") == 0){
attrs = ibus_text_get_attributes(text);
a = attrs == NULL ? NULL : ibus_attr_list_get(attrs, 0);
if(cursor != 1 || !visible || attrs == NULL || a == NULL ||
a->type != IBUS_ATTR_TYPE_UNDERLINE ||
a->value != IBUS_ATTR_UNDERLINE_SINGLE ||
a->start_index != 0 || a->end_index != 1 ||
ibus_attr_list_get(attrs, 1) != NULL)
log->invalid = 1;
else{
log->sawpreedit = 1;
if(log->transfer && ctx == log->a)
log->atext = 1;
}
}
if(log->transfer && log->atext && !log->aclear && s[0] == '\0'){
if(cursor != 0 || visible)
log->invalid = 1;
else{
log->clearctx = ctx;
if(ctx == log->a)
log->aclear = 1;
else
log->invalid = 1;
}
}
log->done = log->sawpreedit && log->sawcommit;
if(log->loop != NULL && log->waiting != NULL && *log->waiting)
g_main_loop_quit(log->loop);
}
static void
modern(IBusInputContext *ctx, IBusText *text, guint cursor,
gboolean visible, guint mode, void *arg)
{
Log *log;
(void)ctx;
(void)text;
(void)cursor;
(void)visible;
(void)mode;
log = arg;
log->modern++;
log->invalid = 1;
if(log->loop != NULL)
g_main_loop_quit(log->loop);
}
static void
commit(IBusInputContext *ctx, IBusText *text, void *arg)
{
IBusAttrList *attrs;
Log *log;
const char *s;
(void)ctx;
log = arg;
log->commit++;
s = ibus_text_get_text(text);
attrs = ibus_text_get_attributes(text);
if(log->hanja){
/* The word converted, with the client\'s own syllable taken back. */
if(strcmp(s, "漢字") == 0 && log->deleted && log->delbefore == -1 &&
log->delcount == 1)
log->converted = 1;
else
log->invalid = 1;
if(log->loop != NULL && log->waiting != NULL && *log->waiting)
g_main_loop_quit(log->loop);
return;
}
if(log->repeat == 3){
/* Focus loss hands the pending syllable back as a commit. */
if(strcmp(s, "") == 0)
log->flushed = 1;
else
log->invalid = 1;
if(log->loop != NULL && log->waiting != NULL && *log->waiting)
g_main_loop_quit(log->loop);
return;
}
if(log->repeat != 0){
revent(log, 'K');
if(log->repeat != 2 || strcmp(s, "") != 0 || attrs == NULL ||
ibus_attr_list_get(attrs, 0) != NULL)
log->invalid = 1;
else
log->rcommit = 1;
if(log->loop != NULL && log->waiting != NULL && *log->waiting)
g_main_loop_quit(log->loop);
return;
}
if(log->retake){
/* Typing again takes back the reading the other context took. */
if(strcmp(s, "k") == 0)
log->retook = 1;
else
log->invalid = 1;
log->retake = 0;
if(log->loop != NULL && log->waiting != NULL && *log->waiting)
g_main_loop_quit(log->loop);
return;
}
if(strcmp(s, "") != 0 || attrs == NULL ||
ibus_attr_list_get(attrs, 0) != NULL)
log->invalid = 1;
else
log->sawcommit = 1;
log->done = log->sawpreedit && log->sawcommit;
if(log->loop != NULL && log->waiting != NULL && *log->waiting)
g_main_loop_quit(log->loop);
}
static void
required(IBusInputContext *ctx, void *arg)
{
Log *log;
(void)ctx;
log = arg;
log->required = 1;
if(log->loop != NULL && log->waiting != NULL && *log->waiting)
g_main_loop_quit(log->loop);
}
static void
deleted(IBusInputContext *ctx, gint offset, guint n, void *arg)
{
Log *log;
(void)ctx;
log = arg;
log->deleted = 1;
log->delbefore = offset;
log->delcount = n;
if(log->loop != NULL && log->waiting != NULL && *log->waiting)
g_main_loop_quit(log->loop);
}
static gboolean
timeout(void *arg)
{
g_main_loop_quit(arg);
return G_SOURCE_REMOVE;
}
static int
waitflag(Log *log, int *flag)
{
guint timer;
if(*flag)
return 1;
log->waiting = flag;
log->loop = g_main_loop_new(NULL, FALSE);
timer = g_timeout_add_seconds(4, timeout, log->loop);
g_main_loop_run(log->loop);
if(g_main_context_find_source_by_id(NULL, timer) != NULL)
g_source_remove(timer);
g_main_loop_unref(log->loop);
log->loop = NULL;
log->waiting = NULL;
return *flag;
}
int
main(int argc, char **argv)
{
IBusBus *bus;
IBusInputContext *a, *b, *c;
IBusText *around;
Log log;
int ok;
if(argc != 2){
fprintf(stderr, "usage: ibus_client_smoke address\n");
return 2;
}
if(setenv("IBUS_ADDRESS", argv[1], 1) < 0 ||
unsetenv("IBUS_ADDRESS_FILE") < 0 ||
unsetenv("DBUS_SESSION_BUS_ADDRESS") < 0 || unsetenv("DISPLAY") < 0){
perror("ibus_client_smoke: environment");
return 1;
}
memset(&log, 0, sizeof log);
b = c = NULL;
ibus_init();
bus = ibus_bus_new();
if(bus == NULL || !ibus_bus_is_connected(bus)){
fprintf(stderr, "ibus_client_smoke: cannot connect to private bus\n");
if(bus != NULL) g_object_unref(bus);
return 1;
}
a = ibus_bus_create_input_context(bus, "strans-libibus-smoke-a");
if(a == NULL){
fprintf(stderr, "ibus_client_smoke: cannot create input context\n");
g_object_unref(bus);
return 1;
}
log.a = a;
g_signal_connect(a, "update-preedit-text", G_CALLBACK(legacy), &log);
g_signal_connect(a, "update-preedit-text-with-mode", G_CALLBACK(modern),
&log);
g_signal_connect(a, "commit-text", G_CALLBACK(commit), &log);
ibus_input_context_set_capabilities(a, IBUS_CAP_PREEDIT_TEXT);
ibus_input_context_focus_in(a);
ok = ibus_input_context_process_key_event(a, 'n', 0, IBUS_CONTROL_MASK) &&
ibus_input_context_process_key_event(a, 'k', 0, 0) &&
ibus_input_context_process_key_event(a, 'a', 0, 0) &&
ibus_input_context_process_key_event(a, '0', 0, 0) &&
waitflag(&log, &log.done);
b = ibus_bus_create_input_context(bus, "strans-libibus-smoke-b");
if(b == NULL)
ok = 0;
else{
g_signal_connect(b, "update-preedit-text", G_CALLBACK(legacy), &log);
g_signal_connect(b, "update-preedit-text-with-mode",
G_CALLBACK(modern), &log);
ibus_input_context_set_capabilities(b, IBUS_CAP_PREEDIT_TEXT);
log.transfer = 1;
ok = ok && ibus_input_context_process_key_event(a, 'k', 0, 0) &&
waitflag(&log, &log.atext);
ibus_input_context_focus_in(b);
ok = ok && ibus_input_context_process_key_event(b, 'n', 0, 0) &&
waitflag(&log, &log.aclear);
ibus_input_context_focus_out(b);
}
ibus_input_context_focus_in(a);
log.retake = 1;
ok = ok && ibus_input_context_process_key_event(a, 's', 0,
IBUS_CONTROL_MASK) && waitflag(&log, &log.retook);
log.repeat = 1;
ok = ok && ibus_input_context_process_key_event(a, 'z', 0, 0) &&
waitflag(&log, &log.rfirst);
log.repeat = 2;
log.revent[0] = '\0';
log.rn = 0;
ok = ok && ibus_input_context_process_key_event(a, 'z', 0, 0) &&
waitflag(&log, &log.rdone) && strcmp(log.revent, "PKP") == 0;
log.repeat = 3;
ibus_input_context_focus_out(a);
ok = ok && waitflag(&log, &log.flushed);
/*
* A word is committed a syllable at a time, so the Hanja key must
* reach into what the client already holds and ask for it back.
*/
c = ibus_bus_create_input_context(bus, "strans-libibus-smoke-c");
if(c == NULL)
ok = 0;
else{
g_signal_connect(c, "commit-text", G_CALLBACK(commit), &log);
g_signal_connect(c, "require-surrounding-text",
G_CALLBACK(required), &log);
g_signal_connect(c, "delete-surrounding-text",
G_CALLBACK(deleted), &log);
log.repeat = 0;
ibus_input_context_set_capabilities(c,
IBUS_CAP_PREEDIT_TEXT|IBUS_CAP_SURROUNDING_TEXT);
ibus_input_context_focus_in(c);
ok = ok && waitflag(&log, &log.required);
around = ibus_text_new_from_string("저는 한");
ibus_input_context_set_surrounding_text(c, around, 4, 4);
log.hanja = 1;
ok = ok && ibus_input_context_process_key_event(c, 's', 0,
IBUS_CONTROL_MASK) &&
ibus_input_context_process_key_event(c, 'w', 0, 0) &&
ibus_input_context_process_key_event(c, 'k', 0, 0) &&
ibus_input_context_process_key_event(c, 'h', 0,
IBUS_CONTROL_MASK) &&
ibus_input_context_process_key_event(c, IBUS_KEY_Return,
0, 0) && waitflag(&log, &log.converted);
log.hanja = 0;
}
if(log.legacy == 0 || log.modern != 0 || log.commit != 5 ||
log.invalid || !log.done || !log.atext || !log.aclear ||
!log.retook || log.clearctx != a || !log.rdone || !log.converted)
ok = 0;
if(c != NULL)
g_object_unref(c);
if(b != NULL)
g_object_unref(b);
g_object_unref(a);
g_object_unref(bus);
if(!ok){
fprintf(stderr,
"ibus_client_smoke: legacy=%d modern=%d commit=%d invalid=%d preedit=%d committed=%d transfer-text=%d a-clear=%d retook=%d repeat=%s flushed=%d required=%d deleted=%d,%d converted=%d\n",
log.legacy, log.modern, log.commit, log.invalid,
log.sawpreedit, log.sawcommit, log.atext, log.aclear,
log.retook, log.revent, log.flushed, log.required,
log.delbefore, log.delcount, log.converted);
return 1;
}
printf("official libibus client preedit, commit, owner clear, take-back and focus-out hand-back: ok\n");
return 0;
}

View File

@@ -1,262 +0,0 @@
#define _GNU_SOURCE
#include <errno.h>
#include <poll.h>
#include <signal.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include "live.h"
#include "livebus.h"
enum
{
Maxconnections = 64,
};
static int
invalidcall(DBusConnection *conn, char *path, char *member)
{
DBusMessage *reply;
const char *name;
int ok;
reply = sendcall(conn, contextcall(path, member), member);
if(reply == NULL)
return 0;
name = dbus_message_get_error_name(reply);
ok = dbus_message_get_type(reply) == DBUS_MESSAGE_TYPE_ERROR &&
name != NULL && strcmp(name, DBUS_ERROR_INVALID_ARGS) == 0;
if(!ok)
fail("malformed %s returned %s, expected %s", member,
name != NULL ? name : "a non-error reply",
DBUS_ERROR_INVALID_ARGS);
dbus_message_unref(reply);
return ok;
}
/* libibus destroys through the Service interface; the path must go away. */
static int
destroyed(DBusConnection *conn, char *path)
{
DBusMessage *m, *reply;
const char *name;
int ok;
m = dbus_message_new_method_call("org.freedesktop.IBus", path,
"org.freedesktop.IBus.Service", "Destroy");
reply = callret(conn, m, "Service.Destroy");
if(reply == NULL)
return 0;
dbus_message_unref(reply);
reply = sendcall(conn, contextcall(path, "FocusIn"), "FocusIn");
if(reply == NULL)
return 0;
name = dbus_message_get_error_name(reply);
ok = name != NULL && strcmp(name, DBUS_ERROR_UNKNOWN_OBJECT) == 0;
if(!ok)
fail("context survived Service.Destroy");
dbus_message_unref(reply);
return ok;
}
static int
overflowrejected(char *address)
{
DBusConnection *conn;
DBusError err;
struct pollfd pfd;
int fd, n, ok, timeout;
int64_t deadline;
dbus_error_init(&err);
conn = dbus_connection_open_private(address, &err);
if(conn == NULL){
ok = dbus_error_is_set(&err);
dbus_error_free(&err);
return ok || fail("overflow connection failed without a D-Bus error");
}
dbus_error_free(&err);
dbus_connection_set_exit_on_disconnect(conn, FALSE);
if(!dbus_connection_get_unix_fd(conn, &fd)){
closebus(&conn);
return fail("overflow connection has no Unix descriptor");
}
ok = 0;
deadline = nowms() + Calltimeout;
for(;;){
if(!dbus_connection_get_is_connected(conn)){
ok = 1;
break;
}
timeout = leftms(deadline);
if(timeout == 0){
fail("65th IBus connection was not rejected");
break;
}
pfd.fd = fd;
pfd.events = POLLIN;
pfd.revents = 0;
n = poll(&pfd, 1, timeout);
if(n < 0 && errno == EINTR)
continue;
if(n < 0){
fail("poll overflow IBus connection: %s", strerror(errno));
break;
}
if(n == 0)
continue;
if(pfd.revents & POLLNVAL){
fail("overflow IBus descriptor became invalid");
break;
}
dbus_connection_read_write_dispatch(conn, 0);
}
closebus(&conn);
return ok;
}
static int
runcontract(char *address)
{
static char *bad[] = {
"ProcessKeyEvent",
"SetEngine",
"SetCapabilities",
"SetCursorLocation",
"SetCursorLocationRelative",
"SetSurroundingText",
};
DBusConnection *conn;
char path[96];
int i, ok;
conn = openbus(address, "contract");
if(conn == NULL)
return 0;
ok = hello(conn, NULL, 0, "contract Hello") &&
createcontext(conn, path, sizeof path, "contract context");
for(i = 0; ok && i < (int)(sizeof bad / sizeof bad[0]); i++)
ok = invalidcall(conn, path, bad[i]);
ok = ok && destroyed(conn, path);
closebus(&conn);
return ok;
}
static int
runcapacity(char *address)
{
DBusConnection *conn[Maxconnections];
int i, ok;
for(i = 0; i < Maxconnections; i++)
conn[i] = NULL;
ok = 0;
for(i = 0; i < Maxconnections; i++){
conn[i] = openbus(address, "capacity");
if(conn[i] == NULL || !hello(conn[i], NULL, 0, "capacity Hello"))
goto out;
}
if(!overflowrejected(address))
goto out;
closebus(&conn[Maxconnections-1]);
if(!createcontext(conn[0], NULL, 0, "capacity context"))
goto out;
conn[Maxconnections-1] = openbus(address, "replacement");
if(conn[Maxconnections-1] == NULL ||
!hello(conn[Maxconnections-1], NULL, 0, "replacement Hello") ||
!createcontext(conn[Maxconnections-1], NULL, 0, "replacement context"))
goto out;
ok = 1;
out:
for(i = 0; i < Maxconnections; i++)
closebus(&conn[i]);
return ok;
}
static int
runclient(Live *l, char *address, char *program)
{
pid_t pid, n;
int status;
int64_t deadline;
pid = fork();
if(pid < 0)
return fail("fork official libibus client: %s", strerror(errno));
if(pid == 0){
if(setenv("XDG_RUNTIME_DIR", l->runtime, 1) < 0 ||
setenv("XDG_CONFIG_HOME", l->config, 1) < 0 ||
setenv("HOME", l->home, 1) < 0 ||
unsetenv("IBUS_ADDRESS_FILE") < 0)
_exit(126);
execl(program, program, address, (char*)0);
dprintf(STDERR_FILENO, "exec %s: %s\n", program, strerror(errno));
_exit(127);
}
deadline = nowms() + Starttimeout;
for(;;){
n = waitpid(pid, &status, WNOHANG);
if(n == pid)
break;
if(n < 0 && errno == EINTR)
continue;
if(n < 0)
return fail("wait official libibus client: %s", strerror(errno));
if(leftms(deadline) == 0){
fail("official libibus client timed out");
kill(pid, SIGKILL);
do
n = waitpid(pid, &status, 0);
while(n < 0 && errno == EINTR);
return 0;
}
pausems(10);
}
if(!WIFEXITED(status) || WEXITSTATUS(status) != 0)
return fail("official libibus client exited with wait status %#x",
status);
return 1;
}
int
main(int argc, char **argv)
{
Daemon daemon;
Live live;
char address[512];
int capacity, ok;
testname = "ibus_live_test";
capacity = argc == 4 && strcmp(argv[1], "--capacity") == 0;
if(argc != 4){
fprintf(stderr, "usage: ibus_live_test strans mapdir libibus-client\n");
fprintf(stderr, " ibus_live_test --capacity strans mapdir\n");
return 2;
}
daemoninit(&daemon, "daemon");
ok = livesetup(&live, "ibus") &&
startdaemon(&live, &daemon, argv[capacity ? 2 : 1],
argv[capacity ? 3 : 2]) &&
waitready(&live, &daemon, address, sizeof address);
if(ok)
ok = capacity ? runcapacity(address) :
runclient(&live, address, argv[3]) && runcontract(address);
if(!ok)
showerrors(&daemon);
if(!stopdaemon(&daemon))
ok = 0;
if(!closeerrors(&daemon))
ok = 0;
if(!liveclean(&live))
ok = 0;
if(!ok)
return 1;
printf(capacity ? "ibus connection capacity: ok\n" :
"ibus official client and malformed call: ok\n");
return 0;
}

View File

@@ -1,439 +0,0 @@
#include "ibus.c"
#include "test.h"
#include <errno.h>
enum
{
Testctrlmask = 1<<2,
};
typedef struct Ibusfix Ibusfix;
struct Ibusfix
{
Channel *oldreply;
Pump pump;
/* A fake connection is nothing but an address to tell two apart. */
char fake[2];
DBusConnection *c1;
DBusConnection *c2;
};
void
ibus_machine_id_fallback(struct ct *t)
{
char root[] = "/tmp/strans-machine-id.XXXXXX";
char primary[128], fallback[128], override[128], got[512];
char *path[2], *old, *saved;
FILE *fp;
int madeprimary, madefallback;
madeprimary = 0;
madefallback = 0;
if(!CT_CHECK(t, mkdtemp(root) != nil))
return;
if(!CT_CHECK(t, snprintf(primary, sizeof primary, "%s/primary", root)
< (int)sizeof primary) ||
!CT_CHECK(t, snprintf(fallback, sizeof fallback, "%s/fallback", root)
< (int)sizeof fallback))
goto cleanup;
if(!CT_CHECK(t, mkdir(primary, 0700) == 0))
goto cleanup;
madeprimary = 1;
fp = fopen(fallback, "w");
if(!CT_CHECK(t, fp != nil))
goto cleanup;
madefallback = 1;
if(!CT_CHECK(t, fputs("fallback-machine-id\n", fp) >= 0)){
fclose(fp);
goto cleanup;
}
if(!CT_CHECK(t, fclose(fp) == 0))
goto cleanup;
path[0] = primary;
path[1] = fallback;
machineidfiles(got, sizeof got, path, nelem(path));
CT_EQ_STR(t, "fallback-machine-id", got);
path[1] = primary;
machineidfiles(got, sizeof got, path, nelem(path));
CT_EQ_STR(t, "", got);
old = getenv("IBUS_ADDRESS_FILE");
saved = old == nil ? nil : strdup(old);
if(old == nil || CT_CHECK(t, saved != nil)){
if(CT_CHECK(t, snprintf(override, sizeof override, "%s/address", root)
< (int)sizeof override) &&
CT_CHECK(t, setenv("IBUS_ADDRESS_FILE", override, 1) == 0)){
CT_EQ_INT(t, 0, buildaddrpath(got, sizeof got));
CT_EQ_STR(t, override, got);
CT_CHECK(t, setenv("IBUS_ADDRESS_FILE", "", 1) == 0);
CT_EQ_INT(t, -1, buildaddrpath(got, sizeof got));
}
if(saved != nil){
setenv("IBUS_ADDRESS_FILE", saved, 1);
free(saved);
}else
unsetenv("IBUS_ADDRESS_FILE");
}
cleanup:
if(madefallback)
CT_CHECK(t, unlink(fallback) == 0);
if(madeprimary)
CT_CHECK(t, rmdir(primary) == 0);
CT_CHECK(t, rmdir(root) == 0);
}
static void
ibusbegin(Ibusfix *f)
{
Drawcmd dc;
memset(f, 0, sizeof *f);
memset(contexts, 0, sizeof contexts);
memset(conns, 0, sizeof conns);
nconns = 0;
preowner = nil;
while(channbrecv(drawc, &dc) > 0)
;
testengineinit(LangEN);
f->oldreply = replyc;
replyc = chancreate(sizeof(Keyres), 0);
pumpstart(&f->pump, Maxcontexts);
f->c1 = (DBusConnection*)&f->fake[0];
f->c2 = (DBusConnection*)&f->fake[1];
}
static void
ibusend(Ibusfix *f)
{
Drawcmd dc;
int i;
for(i = 0; i < nelem(contexts); i++)
if(contexts[i].conn != nil)
dropcontext(&contexts[i]);
pumpstop(&f->pump);
memset(contexts, 0, sizeof contexts);
memset(conns, 0, sizeof conns);
nconns = 0;
preowner = nil;
testengineinit(LangEN);
while(channbrecv(drawc, &dc) > 0)
;
chanfree(replyc);
replyc = f->oldreply;
}
static Keyreq
nexttrace(struct ct *t, Ibusfix *f, int op, Ictx *ctx)
{
Keyreq req;
memset(&req, 0, sizeof req);
if(!CT_CHECK(t, channbrecv(f->pump.trace, &req) > 0))
return req;
CT_EQ_INT(t, op, req.op);
CT_EQ_PTR(t, ctx, req.owner);
if(ctx != nil)
CT_EQ_INT(t, clientpreedit(ctx), req.clientpre);
CT_EQ_PTR(t, replyc, req.reply);
return req;
}
static void
notrace(struct ct *t, Ibusfix *f)
{
Keyreq req;
CT_CHECK(t, channbrecv(f->pump.trace, &req) <= 0);
}
static Keyres
contextkey(struct ct *t, Ibusfix *f, Ictx *ctx, u32int sym, u32int state)
{
Keyres res;
char text[Maxutf];
memset(&res, 0, sizeof res);
CT_CHECK(t, processkey(ctx, sym, state, text, sizeof text, &res));
nexttrace(t, f, Keypress, ctx);
return res;
}
void
ibus_capability_policy(struct ct *t)
{
Ibusfix f;
Ictx *a, *b;
Keyres res;
ibusbegin(&f);
a = newcontext(f.c1, "/context/cap-a");
b = newcontext(f.c2, "/context/cap-b");
if(!CT_CHECK(t, a != nil && b != nil))
goto cleanup;
CT_EQ_INT(t, 0, a->cap);
a->focused = 1;
res = contextkey(t, &f, a, 'n', Testctrlmask);
CT_CHECK(t, res.eaten);
res = contextkey(t, &f, a, 'k', 0);
checkstr(t, "preedit", "k", &res.preedit);
CT_EQ_PTR(t, a, testengineowner());
a->cap = Ibuscappreedit;
memset(&res, 0, sizeof res);
sendrequest(a, Keycap, 0, 0, &res);
nexttrace(t, &f, Keycap, a);
CT_CHECK(t, res.eaten);
checkstr(t, "preedit", "k", &res.preedit);
b->focused = 1;
b->cap = Ibuscappreedit;
memset(&res, 0, sizeof res);
sendrequest(b, Keycap, 0, 0, &res);
nexttrace(t, &f, Keycap, b);
CT_CHECK(t, !res.eaten);
CT_EQ_PTR(t, a, testengineowner());
checkenginepreedit(t, "k");
a->cap = 0;
memset(&res, 0, sizeof res);
sendrequest(a, Keycap, 0, 0, &res);
nexttrace(t, &f, Keycap, a);
CT_CHECK(t, res.eaten);
checkstr(t, "preedit", "k", &res.preedit);
cleanup:
ibusend(&f);
}
void
ibus_private_input_policy(struct ct *t)
{
static const struct { u32int purpose; int want; } cases[] = {
{ 0, 0 },
{ Purposepassword, 1 },
{ Purposepin, 1 },
{ 37, 0 },
};
Ibusfix f;
Ictx *ctx;
Keyres res;
char text[Maxutf];
int i;
ibusbegin(&f);
ctx = newcontext(f.c1, "/context/private");
if(!CT_CHECK(t, ctx != nil))
goto cleanup;
for(i = 0; i < nelem(cases); i++){
ctx->purpose = cases[i].purpose;
CT_EQ_INT(t, cases[i].want, hidden(ctx));
}
ctx->focused = 1;
ctx->cap = 1;
ctx->purpose = 0;
contextkey(t, &f, ctx, 'n', Testctrlmask);
res = contextkey(t, &f, ctx, 'k', 0);
checkstr(t, "preedit", "k", &res.preedit);
ctx->purpose = Purposepassword;
memset(&res, 0, sizeof res);
CT_CHECK(t, !processkey(ctx, 'x', 0, text, sizeof text, &res));
CT_CHECK(t, !res.eaten);
notrace(t, &f);
checkenginepreedit(t, "k");
ctx->purpose = 0;
res = contextkey(t, &f, ctx, 'a', 0);
checkstr(t, "preedit", "", &res.preedit);
cleanup:
ibusend(&f);
}
/* A second release is silent, and dropping frees the slot. */
static void
checkdrop(struct ct *t, Ibusfix *f, Ictx *ctx, char *path)
{
releasecontext(ctx);
notrace(t, f);
dropcontext(ctx);
notrace(t, f);
CT_EQ_PTR(t, nil, findcontext(f->c1, path));
}
void
ibus_context_lifecycle(struct ct *t)
{
Ibusfix f;
Ictx *a, *b, *reused;
Keyreq req;
Keyres res;
Caret at;
char text[Maxutf];
ibusbegin(&f);
a = newcontext(f.c1, "/context/one");
b = newcontext(f.c2, "/context/one");
if(!CT_CHECK(t, a != nil && b != nil && a != b))
goto cleanup;
CT_EQ_PTR(t, a, findcontext(f.c1, "/context/one"));
CT_EQ_PTR(t, b, findcontext(f.c2, "/context/one"));
CT_EQ_PTR(t, nil, findcontext(f.c1, "/context/missing"));
setcursor(a, -10, -20, 14);
notrace(t, &f);
CT_CHECK(t, a->caret.valid);
memset(&res, 0, sizeof res);
CT_CHECK(t, !processkey(a, 'x', Relmask, text, sizeof text, &res));
CT_CHECK(t, !res.eaten);
notrace(t, &f);
CT_CHECK(t, !a->focused);
CT_EQ_PTR(t, nil, testengineowner());
/* A key stands for the FocusIn a client may never send. */
memset(&res, 0, sizeof res);
CT_CHECK(t, processkey(a, 'x', 0, text, sizeof text, &res));
req = nexttrace(t, &f, Keypress, a);
CT_CHECK(t, a->focused);
CT_CHECK(t, !res.eaten);
CT_CHECK(t, req.caret.valid);
CT_EQ_INT(t, -10, req.caret.x);
CT_EQ_INT(t, -20, req.caret.y);
CT_EQ_INT(t, 14, req.caret.h);
CT_EQ_PTR(t, a, testengineowner());
testenginecaret(&at);
CT_CHECK(t, at.valid);
CT_EQ_INT(t, -10, at.x);
CT_EQ_INT(t, -20, at.y);
res = contextkey(t, &f, a, 'n', Testctrlmask);
CT_CHECK(t, res.eaten);
contextkey(t, &f, a, 'k', 0);
res = contextkey(t, &f, a, 'a', 0);
checkstr(t, "preedit", "", &res.preedit);
b->focused = 1;
res = contextkey(t, &f, b, 'n', 0);
checkstr(t, "preedit", "", &res.preedit);
CT_EQ_PTR(t, b, testengineowner());
setcursor(b, 50, 60, 12);
req = nexttrace(t, &f, Keycaret, b);
CT_CHECK(t, req.caret.valid);
CT_EQ_INT(t, 50, req.caret.x);
testenginecaret(&at);
CT_CHECK(t, at.valid);
CT_EQ_INT(t, 50, at.x);
memset(&res, 0, sizeof res);
sendrequest(a, Keyreset, 0, 0, &res);
nexttrace(t, &f, Keyreset, a);
checkstr(t, "preedit", "", &res.preedit);
checkenginepreedit(t, "");
CT_EQ_PTR(t, b, testengineowner());
setcursor(a, 90, 100, 20);
nexttrace(t, &f, Keycaret, a);
testenginecaret(&at);
CT_EQ_INT(t, 50, at.x);
CT_EQ_INT(t, 60, at.y);
releasecontext(a);
nexttrace(t, &f, Keyrelease, a);
CT_CHECK(t, !a->focused);
CT_CHECK(t, !a->caret.valid);
checkenginepreedit(t, "");
CT_EQ_PTR(t, b, testengineowner());
checkdrop(t, &f, a, "/context/one");
reused = newcontext(f.c1, "/context/stale-destroy");
CT_EQ_PTR(t, a, reused);
reused->focused = 1;
dropcontext(reused);
nexttrace(t, &f, Keyrelease, reused);
notrace(t, &f);
CT_EQ_PTR(t, nil, findcontext(f.c1, "/context/stale-destroy"));
checkenginepreedit(t, "");
CT_EQ_PTR(t, b, testengineowner());
reused = newcontext(f.c1, "/context/reused");
CT_EQ_PTR(t, a, reused);
reused->focused = 1;
dropconncontexts(f.c1);
nexttrace(t, &f, Keyrelease, reused);
notrace(t, &f);
CT_EQ_PTR(t, nil, findcontext(f.c1, "/context/reused"));
checkenginepreedit(t, "");
CT_EQ_PTR(t, b, testengineowner());
dropconncontexts(f.c1);
notrace(t, &f);
memset(&res, 0, sizeof res);
CT_CHECK(t, !processkey(b, 'x', Relmask, text, sizeof text, &res));
CT_CHECK(t, !res.eaten);
notrace(t, &f);
CT_EQ_PTR(t, b, testengineowner());
cleanup:
ibusend(&f);
}
void
ibus_active_release_lifecycle(struct ct *t)
{
Ibusfix f;
Ictx *ctx, *reused;
Keyres res;
ibusbegin(&f);
ctx = newcontext(f.c1, "/context/reset");
if(!CT_CHECK(t, ctx != nil))
goto cleanup;
ctx->focused = 1;
contextkey(t, &f, ctx, 'n', Testctrlmask);
res = contextkey(t, &f, ctx, 'n', 0);
checkstr(t, "preedit", "", &res.preedit);
memset(&res, 0, sizeof res);
sendrequest(ctx, Keyreset, 0, 0, &res);
nexttrace(t, &f, Keyreset, ctx);
checkstr(t, "preedit", "", &res.preedit);
checkenginepreedit(t, "");
CT_CHECK(t, ctx->focused);
CT_EQ_PTR(t, ctx, testengineowner());
res = contextkey(t, &f, ctx, 'k', 0);
checkstr(t, "preedit", "k", &res.preedit);
CT_EQ_PTR(t, ctx, testengineowner());
releasecontext(ctx);
nexttrace(t, &f, Keyrelease, ctx);
CT_CHECK(t, !ctx->focused);
checkenginepreedit(t, "");
CT_EQ_PTR(t, nil, testengineowner());
checkdrop(t, &f, ctx, "/context/reset");
reused = newcontext(f.c1, "/context/destroy");
CT_EQ_PTR(t, ctx, reused);
reused->focused = 1;
contextkey(t, &f, reused, 'n', 0);
CT_EQ_PTR(t, reused, testengineowner());
dropcontext(reused);
nexttrace(t, &f, Keyrelease, reused);
CT_EQ_PTR(t, nil, testengineowner());
notrace(t, &f);
CT_EQ_PTR(t, nil, findcontext(f.c1, "/context/destroy"));
reused = newcontext(f.c1, "/context/prune");
CT_EQ_PTR(t, ctx, reused);
CT_EQ_PTR(t, nil, testengineowner());
contextkey(t, &f, reused, 'n', 0);
CT_CHECK(t, reused->focused);
CT_EQ_PTR(t, reused, testengineowner());
dropconncontexts(f.c1);
nexttrace(t, &f, Keyrelease, reused);
CT_EQ_PTR(t, nil, testengineowner());
notrace(t, &f);
CT_EQ_PTR(t, nil, findcontext(f.c1, "/context/prune"));
dropconncontexts(f.c1);
notrace(t, &f);
cleanup:
ibusend(&f);
}

View File

@@ -1,390 +0,0 @@
#define _GNU_SOURCE
#include <errno.h>
#include <poll.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include "ipc.h"
#include "live.h"
enum
{
Maxclients = 64,
Cappreedit = 1,
};
typedef struct Response Response;
struct Response
{
int eaten;
int del;
char commit[Ipcfieldmax+1];
char preedit[Ipcfieldmax+1];
};
static size_t
getlen(unsigned char p[Ipclensz])
{
return p[0] | (p[1] << 8);
}
static int
readresponseuntil(int fd, int want, Response *res, int64_t deadline,
int quiet)
{
unsigned char hdr[Ipcresphdrsz], len[Ipclensz];
size_t n;
int rv;
memset(res, 0, sizeof *res);
rv = readuntil(fd, hdr, sizeof hdr, deadline);
if(rv <= 0){
if(!quiet)
fail("read response header: %s",
rv == 0 ? "peer closed" : strerror(errno));
return rv;
}
res->eaten = hdr[0] != 0;
res->del = hdr[1];
n = getlen(hdr + 2);
if(n > Ipcfieldmax){
fail("invalid commit length %zu", n);
return -1;
}
if(n != 0){
rv = readuntil(fd, res->commit, n, deadline);
if(rv <= 0){
if(!quiet)
fail("read commit: %s",
rv == 0 ? "peer closed" : strerror(errno));
return rv;
}
}
res->commit[n] = '\0';
if(!want)
return 1;
rv = readuntil(fd, len, sizeof len, deadline);
if(rv <= 0){
if(!quiet)
fail("read preedit length: %s",
rv == 0 ? "peer closed" : strerror(errno));
return rv;
}
n = getlen(len);
if(n > Ipcfieldmax){
fail("invalid preedit length %zu", n);
return -1;
}
if(n != 0){
rv = readuntil(fd, res->preedit, n, deadline);
if(rv <= 0){
if(!quiet)
fail("read preedit: %s",
rv == 0 ? "peer closed" : strerror(errno));
return rv;
}
}
res->preedit[n] = '\0';
return 1;
}
static int
sendkey(int fd, int want, uint32_t mod, uint32_t key)
{
unsigned char req[Ipcreqsz];
ipcpackreq(req, want, mod, key);
return ipcsend(fd, req, sizeof req) == 0;
}
static int
sendreset(int fd, int want)
{
unsigned char req[Ipcreqsz];
ipcpackreset(req, want);
return ipcsend(fd, req, sizeof req) == 0;
}
static int
sendcap(int fd, int cap)
{
unsigned char req[Ipcreqsz];
ipcpackcap(req, cap);
return ipcsend(fd, req, sizeof req) == 0;
}
static int
sendsurround(int fd, char *text)
{
unsigned char req[Ipcreqsz];
size_t n;
n = strlen(text);
ipcpacksurround(req, n);
return ipcsend(fd, req, sizeof req) == 0 &&
(n == 0 || ipcsend(fd, text, n) == 0);
}
static int
expectresponse(int fd, int want, int eaten, char *commit, char *preedit,
char *where)
{
Response res;
if(readresponseuntil(fd, want, &res, nowms() + Calltimeout, 0) != 1)
return 0;
if(res.eaten != eaten || strcmp(res.commit, commit) != 0 ||
(want && strcmp(res.preedit, preedit) != 0))
return fail("%s: eaten=%d commit=%s preedit=%s",
where, res.eaten, res.commit, res.preedit);
return 1;
}
static int
requestreset(int fd, int want, int eaten, char *commit, char *preedit,
char *where)
{
if(!sendreset(fd, want))
return fail("send %s: %s", where, strerror(errno));
return expectresponse(fd, want, eaten, commit, preedit, where);
}
static int
requestkey(int fd, int want, uint32_t mod, uint32_t key, int eaten,
char *commit, char *preedit, char *where)
{
if(!sendkey(fd, want, mod, key))
return fail("send %s: %s", where, strerror(errno));
return expectresponse(fd, want, eaten, commit, preedit, where);
}
/*
* The Korean habit: 한자 typed, then the Hanja key. The 한 is the
* client's by then, so the daemon must ask for it back and answer with
* the whole word.
*/
static int
requesthanja(int fd)
{
Response res;
if(!sendsurround(fd, "저는 한"))
return fail("send surrounding text: %s", strerror(errno));
if(!requestkey(fd, 1, Mctrl, 's', 1, "", "", "select Korean") ||
!requestkey(fd, 1, 0, 'w', 1, "", "", "jamo") ||
!requestkey(fd, 1, 0, 'k', 1, "", "", "syllable") ||
!requestkey(fd, 1, Mctrl, 'h', 1, "", "", "Hanja search"))
return 0;
if(!sendkey(fd, 1, 0, Kret))
return fail("send Hanja pick: %s", strerror(errno));
if(readresponseuntil(fd, 1, &res, nowms() + Calltimeout, 0) != 1)
return 0;
if(strcmp(res.commit, "漢字") != 0 || res.del != 1)
return fail("Hanja pick committed %s and took back %d",
res.commit, res.del);
return 1;
}
static int
tryclient(char *path, int64_t deadline, int *client)
{
Response res;
int fd, rv;
fd = connectsocket(path, deadline);
if(fd < 0)
return errno == ETIMEDOUT ? -1 : 0;
if(!sendkey(fd, 1, 0, 0)){
close(fd);
return 0;
}
rv = readresponseuntil(fd, 1, &res, deadline, 1);
if(rv != 1){
close(fd);
return rv < 0 && errno == ETIMEDOUT ? -1 : 0;
}
if(res.eaten || res.commit[0] != '\0' || res.preedit[0] != '\0'){
close(fd);
fail("replacement client returned invalid modifier response");
return -1;
}
*client = fd;
return 1;
}
static int
waitslot(char *path, int *client, char *where)
{
int rv;
int64_t deadline;
deadline = nowms() + Calltimeout;
for(;;){
rv = tryclient(path, deadline, client);
if(rv > 0)
return 1;
if(rv < 0 || leftms(deadline) == 0)
return fail("%s: timed out waiting for a worker slot", where);
}
}
static int
overflowrejected(char *path)
{
struct pollfd pfd;
unsigned char byte;
ssize_t n;
int fd, timeout;
int64_t deadline;
deadline = nowms() + Calltimeout;
fd = connectsocket(path, deadline);
if(fd < 0)
return fail("overflow connect: %s", strerror(errno));
if(!sendkey(fd, 1, 0, 0)){
close(fd);
return 1;
}
for(;;){
timeout = leftms(deadline);
if(timeout == 0){
close(fd);
return fail("overflow client was not rejected");
}
pfd.fd = fd;
pfd.events = POLLIN;
pfd.revents = 0;
n = poll(&pfd, 1, timeout);
if(n < 0 && errno == EINTR)
continue;
if(n <= 0){
close(fd);
return fail("poll overflow client: %s",
n == 0 ? "timed out" : strerror(errno));
}
n = recv(fd, &byte, 1, 0);
if(n < 0 && errno == EINTR)
continue;
if(n == 0 || (n < 0 && (errno == ECONNRESET || errno == EPIPE))){
close(fd);
return 1;
}
if(n < 0){
close(fd);
return fail("read overflow rejection: %s", strerror(errno));
}
close(fd);
return fail("overflow client received a response");
}
}
static int
runsmoke(char *path)
{
int client, ok;
client = connectsocket(path, nowms() + Calltimeout);
if(client < 0)
return fail("connect client: %s", strerror(errno));
ok = sendcap(client, Cappreedit) &&
expectresponse(client, 1, 1, "", "", "negotiate preedit") &&
requestkey(client, 1, Mctrl, 'n', 1, "", "",
"select Japanese") &&
requestkey(client, 1, 0, 'k', 1, "", "k", "preedit") &&
requestreset(client, 1, 1, "k", "", "reset") &&
requesthanja(client);
close(client);
return ok;
}
static int
runcapacity(char *path)
{
int client[Maxclients];
int i, ok;
for(i = 0; i < Maxclients; i++)
client[i] = -1;
ok = 0;
client[0] = connectsocket(path, nowms() + Calltimeout);
if(client[0] < 0){
fail("connect first client: %s", strerror(errno));
goto out;
}
if(!requestkey(client[0], 1, Mctrl, 'n', 1, "", "",
"select Japanese"))
goto out;
for(i = 1; i < Maxclients; i++){
client[i] = connectsocket(path, nowms() + Calltimeout);
if(client[i] < 0){
fail("connect capacity client %d: %s", i, strerror(errno));
goto out;
}
if(!requestkey(client[i], 1, 0, 0, 0, "", "",
"capacity modifier"))
goto out;
}
if(!overflowrejected(path))
goto out;
close(client[Maxclients-1]);
client[Maxclients-1] = -1;
if(!waitslot(path, &client[Maxclients-1], "inactive slot recovery"))
goto out;
close(client[0]);
client[0] = -1;
if(!waitslot(path, &client[0], "active slot recovery") ||
!requestkey(client[0], 1, 0, 'a', 1, "", "",
"post-disconnect composition"))
goto out;
ok = 1;
out:
for(i = 0; i < Maxclients; i++)
if(client[i] >= 0)
close(client[i]);
return ok;
}
int
main(int argc, char **argv)
{
Daemon daemon;
Live live;
int capacity, ok;
testname = "ipc_live_test";
capacity = argc == 4 && strcmp(argv[1], "--capacity") == 0;
if((!capacity && argc != 3) || (capacity && argc != 4)){
fprintf(stderr,
"usage: ipc_live_test [--capacity] strans mapdir\n");
return 2;
}
daemoninit(&daemon, "daemon");
ok = livesetup(&live, "ipc") &&
startdaemon(&live, &daemon, argv[1+capacity], argv[2+capacity]) &&
waitready(&live, &daemon, NULL, 0);
if(ok)
ok = capacity ? runcapacity(live.socket) : runsmoke(live.socket);
if(!ok)
showerrors(&daemon);
if(!stopdaemon(&daemon))
ok = 0;
/* SIGTERM must leave no socket behind. */
if(access(live.socket, F_OK) == 0)
ok = fail("socket %s survived the daemon", live.socket);
if(!closeerrors(&daemon))
ok = 0;
if(!liveclean(&live))
ok = 0;
if(!ok)
return 1;
printf("ipc %s: ok\n", capacity ? "connection capacity" : "live smoke");
return 0;
}

View File

@@ -1,447 +0,0 @@
#define _POSIX_C_SOURCE 200809L
#include <errno.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <time.h>
#include <unistd.h>
#include "dat.h"
#include "fn.h"
#include "test.h"
#undef accept
#undef listen
#undef send
static int64_t
nowms(void)
{
struct timespec ts;
if(clock_gettime(CLOCK_MONOTONIC, &ts) < 0)
return 0;
return (int64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
}
static void ipcclientdeadlines(struct ct*);
static void ipcconnectcloexec(struct ct*);
void
ipc_masks_modifiers(struct ct *t)
{
uchar buf[Ipcreqsz];
u32int key, mod;
int want;
ipcpackreq(buf, 1, Mctrl|Malt|(1<<1)|(1<<4)|(1<<7), 0x1f642);
ipcunpackreq(buf, &want, &mod, &key);
CT_EQ_INT(t, 1, want);
CT_EQ_UINT(t, Mctrl|Malt, mod);
CT_EQ_UINT(t, 0x1f642, key);
CT_CHECK(t, !ipcreqreset(buf));
ipcpackreset(buf, 1);
ipcunpackreq(buf, &want, &mod, &key);
CT_EQ_INT(t, 1, want);
CT_EQ_UINT(t, 0, mod);
CT_EQ_UINT(t, 0, key);
CT_CHECK(t, ipcreqreset(buf));
/* Frontends share one keysym and modifier mapping. */
CT_EQ_UINT(t, 'a', ipckeysym('a', 'a'));
CT_EQ_UINT(t, '1', ipckeysym(0xffb1, '1')); /* KP_1 */
CT_EQ_UINT(t, Kret, ipckeysym(0xff0d, '\r'));
CT_EQ_UINT(t, Kret, ipckeysym(0xff8d, '\r')); /* KP_Enter */
CT_EQ_UINT(t, Ktab, ipckeysym(0xfe20, 0)); /* ISO_Left_Tab */
CT_EQ_UINT(t, Kdown, ipckeysym(0xff99, 0)); /* KP_Down */
CT_EQ_UINT(t, Kback, ipckeysym(0xff08, 8));
CT_EQ_UINT(t, Kspec + 0xff, ipckeysym(0xffff, 0x7f)); /* Delete */
CT_EQ_UINT(t, 0x1f642, ipckeysym(0x101f642, 0x1f642));
CT_EQ_UINT(t, 0, ipckeysym(0xfe03, 0)); /* ISO_Level3_Shift */
CT_EQ_UINT(t, 0, ipckeysym(0xffe1, 0)); /* Shift_L */
CT_EQ_UINT(t, Mshift|Mctrl, ipcmod(Mshift|Mctrl|(1<<1)|(1<<4)));
CT_EQ_UINT(t, Msuper, ipcmod(1<<6));
CT_EQ_UINT(t, Msuper, ipcmod(1<<26));
}
void
ipc_control_and_caret_frames(struct ct *t)
{
static const uchar capon[Ipcreqsz] = {
Ipcext|Ipcreqwant, Ipcversion, Ipcopcap, 0, 0, 0,
};
static const uchar capoff[Ipcreqsz] = {
Ipcext, Ipcversion, Ipcopcap, 0, 0, 0,
};
static const uchar caret[Ipccaretsz] = {
Ipcext, Ipcversion, Ipcopcaret, 1,
0xfe, 0xff, 0xff, 0xff,
0x04, 0x03, 0x02, 0x01,
0x06, 0x05, 0x00, 0x00,
};
static const uchar nocaret[Ipccaretsz] = {
Ipcext, Ipcversion, Ipcopcaret, 0,
};
uchar buf[Ipccaretsz], bad[Ipccaretsz];
u32int key, mod;
int valid, want;
int32_t x, y, h;
ipcpackcap(buf, 1);
CT_EQ_MEM(t, capon, buf, sizeof capon);
CT_EQ_INT(t, Ipccap, ipcreqtype(buf));
/* The probe remains a harmless key-zero request to an old daemon. */
ipcunpackreq(buf, &want, &mod, &key);
CT_EQ_INT(t, 1, want);
CT_EQ_UINT(t, 0, key);
ipcpackcap(buf, 0);
CT_EQ_MEM(t, capoff, buf, sizeof capoff);
ipcunpackreq(buf, &want, &mod, &key);
CT_EQ_INT(t, 0, want);
CT_EQ_UINT(t, 0, key);
ipcpackcaret(buf, 1, -2, 0x01020304, 0x506);
CT_EQ_MEM(t, caret, buf, sizeof caret);
CT_EQ_INT(t, Ipccaret, ipcreqtype(buf));
if(CT_EQ_INT(t, 0, ipcunpackcaret(buf, &valid, &x, &y, &h))){
CT_EQ_INT(t, 1, valid);
CT_EQ_INT(t, -2, x);
CT_EQ_INT(t, 0x01020304, y);
CT_EQ_INT(t, 0x506, h);
}
ipcpackcaret(buf, 1, INT32_MIN, INT32_MAX, 0);
if(CT_EQ_INT(t, 0, ipcunpackcaret(buf, &valid, &x, &y, &h))){
CT_EQ_INT(t, INT32_MIN, x);
CT_EQ_INT(t, INT32_MAX, y);
CT_EQ_INT(t, 0, h);
}
ipcpackcaret(buf, 0, -2, 3, 4);
CT_EQ_MEM(t, nocaret, buf, sizeof nocaret);
if(CT_EQ_INT(t, 0, ipcunpackcaret(buf, &valid, &x, &y, &h)))
CT_EQ_INT(t, 0, valid);
memcpy(bad, caret, sizeof bad);
bad[1]++;
CT_EQ_INT(t, Ipcunknown, ipcreqtype(bad));
CT_EQ_INT(t, -1, ipcunpackcaret(bad, &valid, &x, &y, &h));
memcpy(bad, caret, sizeof bad);
bad[2]++;
CT_EQ_INT(t, Ipcunknown, ipcreqtype(bad));
CT_EQ_INT(t, -1, ipcunpackcaret(bad, &valid, &x, &y, &h));
memcpy(bad, caret, sizeof bad);
bad[3] = 2;
CT_EQ_INT(t, -1, ipcunpackcaret(bad, &valid, &x, &y, &h));
memcpy(bad, caret, sizeof bad);
bad[0] |= Ipcreqwant;
CT_EQ_INT(t, Ipcunknown, ipcreqtype(bad));
memcpy(bad, caret, sizeof bad);
bad[15] = 0x80;
CT_EQ_INT(t, -1, ipcunpackcaret(bad, &valid, &x, &y, &h));
memcpy(bad, capon, sizeof capon);
bad[3] = 1;
CT_EQ_INT(t, Ipcunknown, ipcreqtype(bad));
ipcpackreq(buf, 1, 0, 'a');
CT_EQ_INT(t, Ipckey, ipcreqtype(buf));
ipcpackreset(buf, 1);
CT_EQ_INT(t, Ipckey, ipcreqtype(buf));
}
void
ipc_runtime_path(struct ct *t)
{
char buf[128], fallback[128], *old, *saved;
old = getenv("XDG_RUNTIME_DIR");
saved = old == nil ? nil : strdup(old);
setenv("XDG_RUNTIME_DIR", "/tmp/strans-runtime-test", 1);
CT_EQ_INT(t, 0, ipcpath(buf, sizeof buf));
CT_EQ_STR(t, "/tmp/strans-runtime-test/strans.sock", buf);
setenv("XDG_RUNTIME_DIR", "/tmp/strans-runtime-test/", 1);
CT_EQ_INT(t, 0, ipcpath(buf, sizeof buf));
CT_EQ_STR(t, "/tmp/strans-runtime-test/strans.sock", buf);
setenv("XDG_RUNTIME_DIR", "relative", 1);
CT_EQ_INT(t, 0, ipcpath(buf, sizeof buf));
snprint(fallback, sizeof fallback, "/tmp/strans.%d", getuid());
CT_EQ_STR(t, fallback, buf);
CT_EQ_INT(t, -1, ipcpath(buf, 4));
if(saved != nil){
setenv("XDG_RUNTIME_DIR", saved, 1);
free(saved);
}else
unsetenv("XDG_RUNTIME_DIR");
}
void
ipc_response_pack_boundaries(struct ct *t)
{
static const uchar withpre[] = { 1, 3, 1, 0, 'A', 2, 0, 'x', 'y' };
static const uchar withoutpre[] = { 1, 0, 1, 0, 'A' };
uchar out[Ipcmaxresp+1], field[Ipcfieldmax];
int n;
n = ipcpackresp(out, sizeof out, 7, 3, "A", 1, "xy", 2, 1);
CT_EQ_INT(t, sizeof withpre, n);
CT_EQ_MEM(t, withpre, out, sizeof withpre);
n = ipcpackresp(out, sizeof out, 1, 0, "A", 1, "xy", 2, 0);
CT_EQ_INT(t, sizeof withoutpre, n);
CT_EQ_MEM(t, withoutpre, out, sizeof withoutpre);
n = ipcpackresp(out, sizeof out, 0, 0, nil, 0, nil, 0, 1);
CT_EQ_INT(t, Ipcresphdrsz + Ipclensz, n);
memset(field, 'x', sizeof field);
memset(out, 0xa5, sizeof out);
n = ipcpackresp(out, Ipcmaxresp, 1, 0,
(char*)field, sizeof field, (char*)field, sizeof field, 1);
CT_EQ_INT(t, Ipcmaxresp, n);
CT_EQ_INT(t, 0, out[2]);
CT_EQ_INT(t, 1, out[3]);
CT_EQ_INT(t, 0, out[4+Ipcfieldmax]);
CT_EQ_INT(t, 1, out[5+Ipcfieldmax]);
CT_EQ_INT(t, 0xa5, out[Ipcmaxresp]);
CT_EQ_INT(t, -1, ipcpackresp(out, Ipcmaxresp-1, 1, 0,
(char*)field, sizeof field, (char*)field, sizeof field, 1));
CT_EQ_INT(t, -1, ipcpackresp(out, sizeof out, 0, 0,
(char*)field, Ipcfieldmax+1, "", 0, 0));
/* A take-back is a rune count and never leaves its byte. */
CT_EQ_INT(t, -1, ipcpackresp(out, sizeof out, 0, -1, "", 0, "", 0, 0));
CT_EQ_INT(t, -1, ipcpackresp(out, sizeof out, 0, 256, "", 0, "", 0, 0));
}
void
ipc_response_empty_and_preedit(struct ct *t)
{
uchar first[Ipcmaxresp], second[Ipcmaxresp];
char commit[Ipcfieldmax+1], preedit[Ipcfieldmax+1];
Ipcresp resp;
int fd[2], nfirst, nsecond;
nfirst = ipcpackresp(first, sizeof first, 0, 0, "", 0, "", 0, 0);
nsecond = ipcpackresp(second, sizeof second, 1, 0,
"go", 2, "kana", 4, 1);
if(!CT_CHECK(t, nfirst > 0 && nsecond > 0))
return;
if(socketpair(AF_UNIX, SOCK_STREAM, 0, fd) < 0){
CT_ERRORF(t, "socketpair failed");
return;
}
if(CT_EQ_INT(t, 0, ipcsend(fd[0], first, nfirst)) &&
CT_EQ_INT(t, 0, ipcsend(fd[0], second, nsecond)) &&
CT_EQ_INT(t, 0, ipcreadresp(fd[1], 0, commit, preedit, &resp))){
CT_EQ_INT(t, 0, resp.eaten);
CT_EQ_SIZE(t, 0, resp.commitlen);
CT_EQ_SIZE(t, 0, resp.preeditlen);
CT_EQ_STR(t, "", commit);
}
if(CT_EQ_INT(t, 0, ipcreadresp(fd[1], 1, commit, preedit, &resp))){
CT_EQ_INT(t, 1, resp.eaten);
CT_EQ_SIZE(t, 2, resp.commitlen);
CT_EQ_SIZE(t, 4, resp.preeditlen);
CT_EQ_STR(t, "go", commit);
CT_EQ_STR(t, "kana", preedit);
}
close(fd[0]);
close(fd[1]);
}
void
ipc_response_max_and_drain(struct ct *t)
{
uchar first[Ipcmaxresp], second[Ipcmaxresp], field[Ipcfieldmax];
char commit[Ipcfieldmax+1], preedit[Ipcfieldmax+1];
Ipcresp resp;
int fd[2], nfirst, nsecond;
memset(field, 'x', sizeof field);
nfirst = ipcpackresp(first, sizeof first, 1, 0,
(char*)field, sizeof field, (char*)field, sizeof field, 1);
nsecond = ipcpackresp(second, sizeof second, 0, 0, "ok", 2, "", 0, 0);
if(!CT_CHECK(t, nfirst == Ipcmaxresp && nsecond > 0))
return;
if(socketpair(AF_UNIX, SOCK_STREAM, 0, fd) < 0){
CT_ERRORF(t, "socketpair failed");
return;
}
if(CT_EQ_INT(t, 0, ipcsend(fd[0], first, nfirst)) &&
CT_EQ_INT(t, 0, ipcsend(fd[0], second, nsecond)) &&
CT_EQ_INT(t, 0, ipcreadresp(fd[1], 1, commit, preedit, &resp))){
CT_EQ_SIZE(t, Ipcfieldmax, resp.commitlen);
CT_EQ_SIZE(t, Ipcfieldmax, resp.preeditlen);
CT_EQ_MEM(t, field, commit, Ipcfieldmax);
CT_EQ_MEM(t, field, preedit, Ipcfieldmax);
CT_EQ_INT(t, 0, commit[Ipcfieldmax]);
CT_EQ_INT(t, 0, preedit[Ipcfieldmax]);
}
if(CT_EQ_INT(t, 0, ipcreadresp(fd[1], 0, commit, preedit, &resp)))
CT_EQ_STR(t, "ok", commit);
close(fd[0]);
close(fd[1]);
}
void
ipc_response_fragmented_and_truncated(struct ct *t)
{
uchar frame[Ipcmaxresp];
char commit[Ipcfieldmax+1], preedit[Ipcfieldmax+1];
Ipcresp resp;
int fd[2], i, n;
n = ipcpackresp(frame, sizeof frame, 1, 2, "abc", 3, "xy", 2, 1);
if(!CT_CHECK(t, n > 0))
return;
if(socketpair(AF_UNIX, SOCK_STREAM, 0, fd) < 0){
CT_ERRORF(t, "socketpair failed");
return;
}
for(i = 0; i < n; i++)
if(ipcsend(fd[0], frame+i, 1) < 0){
CT_ERRORF(t, "fragment send failed");
break;
}
if(i == n && CT_EQ_INT(t, 0, ipcreadresp(fd[1], 1, commit, preedit,
&resp))){
CT_EQ_INT(t, 2, resp.del);
CT_EQ_STR(t, "abc", commit);
CT_EQ_STR(t, "xy", preedit);
}
close(fd[0]);
close(fd[1]);
if(socketpair(AF_UNIX, SOCK_STREAM, 0, fd) < 0){
CT_ERRORF(t, "socketpair failed");
return;
}
CT_EQ_INT(t, 0, ipcsend(fd[0], frame, n-1));
shutdown(fd[0], SHUT_WR);
CT_EQ_INT(t, -1, ipcreadresp(fd[1], 1, commit, preedit, &resp));
close(fd[0]);
close(fd[1]);
if(socketpair(AF_UNIX, SOCK_STREAM, 0, fd) < 0){
CT_ERRORF(t, "socketpair failed");
return;
}
frame[0] = 2;
CT_EQ_INT(t, 0, ipcsend(fd[0], frame, n));
errno = 0;
CT_EQ_INT(t, -1, ipcreadresp(fd[1], 1, commit, preedit, &resp));
CT_EQ_INT(t, EPROTO, errno);
close(fd[0]);
close(fd[1]);
}
void
ipc_broken_peer_send(struct ct *t)
{
int fd[2];
if(socketpair(AF_UNIX, SOCK_STREAM, 0, fd) < 0){
CT_ERRORF(t, "socketpair failed");
return;
}
close(fd[1]);
CT_EQ_INT(t, -1, ipcsend(fd[0], "x", 1));
close(fd[0]);
ipcclientdeadlines(t);
ipcconnectcloexec(t);
}
static void
ipcclientdeadlines(struct ct *t)
{
char fill[4096], commit[Ipcfieldmax+1], preedit[Ipcfieldmax+1];
Ipcresp resp;
int fd[2];
int64_t start, elapsed;
ssize_t n;
if(socketpair(AF_UNIX, SOCK_STREAM, 0, fd) < 0){
CT_ERRORF(t, "socketpair failed");
return;
}
start = nowms();
errno = 0;
CT_EQ_INT(t, -1, ipcreadresp(fd[0], 0, commit, preedit, &resp));
elapsed = nowms() - start;
CT_EQ_INT(t, ETIMEDOUT, errno);
CT_CHECK(t, elapsed >= 0 && elapsed < 4*Ipcwaitms);
memset(fill, 'x', sizeof fill);
for(;;){
n = send(fd[0], fill, sizeof fill, MSG_DONTWAIT|MSG_NOSIGNAL);
if(n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK))
break;
if(n <= 0){
CT_ERRORF(t, "fill send failed: %s", strerror(errno));
close(fd[0]);
close(fd[1]);
return;
}
}
start = nowms();
errno = 0;
CT_EQ_INT(t, -1, ipcsend(fd[0], "x", 1));
elapsed = nowms() - start;
CT_EQ_INT(t, ETIMEDOUT, errno);
CT_CHECK(t, elapsed >= 0 && elapsed < 4*Ipcwaitms);
close(fd[0]);
close(fd[1]);
}
static void
ipcconnectcloexec(struct ct *t)
{
struct sockaddr_un addr;
char root[] = "/tmp/strans-ipc.XXXXXX";
char *old, *saved;
int accepted, client, flags, full, listener;
accepted = client = full = listener = -1;
old = getenv("XDG_RUNTIME_DIR");
saved = old == nil ? nil : strdup(old);
if(mkdtemp(root) == nil){
CT_ERRORF(t, "mkdtemp failed: %s", strerror(errno));
free(saved);
return;
}
setenv("XDG_RUNTIME_DIR", root, 1);
memset(&addr, 0, sizeof addr);
addr.sun_family = AF_UNIX;
if(ipcpath(addr.sun_path, sizeof addr.sun_path) < 0 ||
(listener = socket(AF_UNIX, SOCK_STREAM, 0)) < 0 ||
bind(listener, (struct sockaddr*)&addr, sizeof addr) < 0 ||
listen(listener, 0) < 0){
CT_ERRORF(t, "listen failed: %s", strerror(errno));
goto Out;
}
client = ipcconnect();
if(!CT_CHECK(t, client >= 0))
goto Out;
flags = fcntl(client, F_GETFD);
CT_CHECK(t, flags >= 0 && (flags & FD_CLOEXEC) != 0);
errno = 0;
full = ipcconnect();
CT_EQ_INT(t, -1, full);
CT_CHECK(t, errno == EAGAIN || errno == EWOULDBLOCK);
accepted = accept(listener, nil, nil);
if(!CT_CHECK(t, accepted >= 0))
goto Out;
Out:
if(full >= 0)
close(full);
if(accepted >= 0)
close(accepted);
if(client >= 0)
close(client);
if(listener >= 0)
close(listener);
unlink(addr.sun_path);
rmdir(root);
if(saved != nil){
setenv("XDG_RUNTIME_DIR", saved, 1);
free(saved);
}else
unsetenv("XDG_RUNTIME_DIR");
}

View File

@@ -1,110 +0,0 @@
#include "dat.h"
#include "fn.h"
#include "test.h"
static Str
kotrans(char *keys)
{
Str out, raw;
raw = mkstr(keys);
transstr(getlang(LangKO), nil, &raw, &out);
return out;
}
void
korean_sequences(struct ct *t)
{
static const struct { char *keys, *want; } cases[] = {
{ "r", "" },
{ "rk", "" },
{ "rkr", "" },
{ "rkrk", "가가" },
{ "rkrtk", "각사" },
{ "rhk", "" },
{ "rr", "ㄱㄱ" },
{ "Rk", "" },
{ "rk1", "가1" },
{ "rt", "" },
{ "rtk", "ㄱ사" },
{ "rtt", "ㄳㅅ" },
{ "kr", "" },
{ "hkr", "" },
{ "kk", "ㅏㅏ" },
};
Str out;
int i;
for(i = 0; i < nelem(cases); i++){
out = kotrans(cases[i].keys);
checkstr(t, cases[i].keys, cases[i].want, &out);
}
}
void
korean_compound_vowels(struct ct *t)
{
static const struct { char *keys, *want; } cases[] = {
{ "hk", "" }, { "ho", "" }, { "hl", "" },
{ "nj", "" }, { "np", "" }, { "nl", "" },
{ "ml", "" },
};
Str out;
int i;
for(i = 0; i < nelem(cases); i++){
out = kotrans(cases[i].keys);
checkstr(t, cases[i].keys, cases[i].want, &out);
}
}
void
korean_compound_finals(struct ct *t)
{
static const struct { char *keys, *want, *split; } cases[] = {
{ "rkrt", "", "각사" }, { "rksw", "", "간자" },
{ "rksg", "", "간하" }, { "rkfr", "", "갈가" },
{ "rkfa", "", "갈마" }, { "rkfq", "", "갈바" },
{ "rkft", "", "갈사" }, { "rkfx", "", "갈타" },
{ "rkfv", "", "갈파" }, { "rkfg", "", "갈하" },
{ "rkqt", "", "갑사" },
};
char keys[16];
Str out;
int i;
for(i = 0; i < nelem(cases); i++){
out = kotrans(cases[i].keys);
checkstr(t, cases[i].keys, cases[i].want, &out);
snprint(keys, sizeof keys, "%sk", cases[i].keys);
out = kotrans(keys);
checkstr(t, keys, cases[i].split, &out);
}
}
void
korean_backspace(struct ct *t)
{
static const struct { char *before, *after; } cases[] = {
{ "", "" },
{ "", "" },
{ "", "" },
{ "", "" }, { "", "" }, { "", "" },
{ "", "" }, { "", "" }, { "", "" },
{ "", "" },
{ "", "" },
{ "", "" }, { "", "" }, { "", "" },
{ "", "" }, { "", "" }, { "", "" },
{ "", "" }, { "", "" }, { "", "" },
{ "", "" }, { "", "" },
};
Im state;
int i;
memset(&state, 0, sizeof state);
for(i = 0; i < nelem(cases); i++){
state.pre = mkstr(cases[i].before);
backko(&state);
checkstr(t, cases[i].before, cases[i].after, &state.pre);
}
}

View File

@@ -1,774 +0,0 @@
#define _GNU_SOURCE
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <poll.h>
#include <signal.h>
#include <stdarg.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/un.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>
#include "ipc.h"
#include "live.h"
char *testname = "live_test";
int
fail(char *fmt, ...)
{
va_list ap;
fprintf(stderr, "%s: ", testname);
va_start(ap, fmt);
vfprintf(stderr, fmt, ap);
va_end(ap);
fputc('\n', stderr);
return 0;
}
int64_t
nowms(void)
{
struct timespec ts;
if(clock_gettime(CLOCK_MONOTONIC, &ts) < 0)
return -1;
return (int64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
}
/* A clock that cannot be read counts as an expired deadline. */
int
leftms(int64_t deadline)
{
int64_t n;
n = nowms();
if(n < 0)
return 0;
n = deadline - n;
if(n <= 0)
return 0;
return n > INT_MAX ? INT_MAX : (int)n;
}
void
pausems(int ms)
{
struct timespec ts;
ts.tv_sec = ms / 1000;
ts.tv_nsec = (ms % 1000) * 1000000L;
while(nanosleep(&ts, &ts) < 0 && errno == EINTR)
;
}
int
makedir(char *path)
{
if(mkdir(path, 0700) == 0)
return 1;
return fail("mkdir %s: %s", path, strerror(errno));
}
int
cleardir(char *path)
{
DIR *dir;
struct dirent *de;
char name[576];
int ok;
if(path[0] == '\0')
return 1;
dir = opendir(path);
if(dir == NULL)
return errno == ENOENT ||
fail("open %s: %s", path, strerror(errno));
ok = 1;
errno = 0;
while((de = readdir(dir)) != NULL){
if(strcmp(de->d_name, ".") == 0 || strcmp(de->d_name, "..") == 0)
continue;
if(snprintf(name, sizeof name, "%s/%s", path, de->d_name)
>= (int)sizeof name)
ok = fail("cleanup path is too long: %s", de->d_name);
else if(unlink(name) < 0 && errno != ENOENT)
ok = fail("remove %s: %s", name, strerror(errno));
errno = 0;
}
if(errno != 0)
ok = fail("read %s: %s", path, strerror(errno));
if(closedir(dir) < 0)
ok = fail("close %s: %s", path, strerror(errno));
return ok;
}
static int
rmdirknown(char *path)
{
if(path[0] == '\0' || rmdir(path) == 0 || errno == ENOENT)
return 1;
return fail("rmdir %s: %s", path, strerror(errno));
}
int
livesetup(Live *l, char *prefix)
{
memset(l, 0, sizeof *l);
if(nowms() < 0)
return fail("read monotonic clock: %s", strerror(errno));
if(snprintf(l->root, sizeof l->root, "/tmp/strans-%s.XXXXXX", prefix)
>= (int)sizeof l->root){
l->root[0] = '\0';
return fail("temporary root name is too long");
}
if(mkdtemp(l->root) == NULL){
l->root[0] = '\0';
return fail("mkdtemp: %s", strerror(errno));
}
if(snprintf(l->runtime, sizeof l->runtime, "%s/runtime", l->root)
>= (int)sizeof l->runtime ||
snprintf(l->config, sizeof l->config, "%s/config", l->root)
>= (int)sizeof l->config ||
snprintf(l->ibus, sizeof l->ibus, "%s/ibus", l->config)
>= (int)sizeof l->ibus ||
snprintf(l->bus, sizeof l->bus, "%s/bus", l->ibus)
>= (int)sizeof l->bus ||
snprintf(l->home, sizeof l->home, "%s/home", l->root)
>= (int)sizeof l->home ||
snprintf(l->socket, sizeof l->socket, "%s/strans.sock", l->runtime)
>= (int)sizeof l->socket)
return fail("temporary path is too long");
return makedir(l->runtime) && makedir(l->config) && makedir(l->ibus) &&
makedir(l->bus) && makedir(l->home);
}
/* The daemon owns its endpoints, so leftovers elsewhere are a failure. */
int
liveclean(Live *l)
{
int ok;
ok = 1;
if(l->socket[0] != '\0' && unlink(l->socket) < 0 && errno != ENOENT)
ok = fail("remove IPC socket %s: %s", l->socket, strerror(errno));
if(!cleardir(l->bus)) ok = 0;
if(!rmdirknown(l->bus)) ok = 0;
if(!rmdirknown(l->ibus)) ok = 0;
if(!rmdirknown(l->config)) ok = 0;
if(!rmdirknown(l->runtime)) ok = 0;
if(!rmdirknown(l->home)) ok = 0;
if(!rmdirknown(l->root)) ok = 0;
return ok;
}
void
daemoninit(Daemon *d, char *name)
{
memset(d, 0, sizeof *d);
d->pid = -1;
d->errfd = -1;
d->name = name;
}
void
readerrors(Daemon *d)
{
ssize_t n;
if(d->errfd < 0)
return;
while(d->nerr + 1 < sizeof d->err){
n = read(d->errfd, d->err + d->nerr, sizeof d->err - d->nerr - 1);
if(n > 0){
d->nerr += n;
continue;
}
if(n < 0 && errno == EINTR)
continue;
break;
}
d->err[d->nerr] = '\0';
}
void
showerrors(Daemon *d)
{
readerrors(d);
if(d->nerr != 0)
fprintf(stderr, "%s: %s stderr:\n%s", testname, d->name, d->err);
}
int
closeerrors(Daemon *d)
{
int fd;
readerrors(d);
fd = d->errfd;
d->errfd = -1;
if(fd >= 0 && close(fd) < 0)
return fail("close %s stderr: %s", d->name, strerror(errno));
return 1;
}
/* Point both standard streams at the capture pipe, even if it is 1 or 2. */
static int
childfds(int fd)
{
int flags;
if(fd > STDERR_FILENO){
if(dup2(fd, STDOUT_FILENO) < 0 || dup2(fd, STDERR_FILENO) < 0)
return 0;
close(fd);
return 1;
}
flags = fcntl(fd, F_GETFD);
if(flags < 0 || fcntl(fd, F_SETFD, flags & ~FD_CLOEXEC) < 0)
return 0;
return dup2(fd, STDOUT_FILENO) >= 0 && dup2(fd, STDERR_FILENO) >= 0;
}
/*
* A private X server for the tests that need one; its display goes in
* Live, so the daemon and the clients started after it share it.
*/
int
startxvfb(Live *l, Daemon *d, char *program)
{
struct pollfd pfd;
char fdarg[16], line[32];
int errpipe[2], n, pipefd[2];
int64_t deadline;
pid_t pid;
ssize_t nr;
if(pipe2(pipefd, O_CLOEXEC) < 0)
return fail("Xvfb display pipe: %s", strerror(errno));
if(pipe2(errpipe, O_CLOEXEC|O_NONBLOCK) < 0){
close(pipefd[0]);
close(pipefd[1]);
return fail("pipe2: %s", strerror(errno));
}
pid = fork();
if(pid < 0){
close(pipefd[0]);
close(pipefd[1]);
close(errpipe[0]);
close(errpipe[1]);
return fail("fork Xvfb: %s", strerror(errno));
}
if(pid == 0){
close(pipefd[0]);
close(errpipe[0]);
n = fcntl(pipefd[1], F_GETFD);
if(n < 0 || fcntl(pipefd[1], F_SETFD, n & ~FD_CLOEXEC) < 0 ||
!childfds(errpipe[1]))
_exit(126);
snprintf(fdarg, sizeof fdarg, "%d", pipefd[1]);
execlp(program, program, "-displayfd", fdarg, "-screen", "0",
"1024x768x24", "-nolisten", "tcp", "-noreset", (char*)0);
_exit(127);
}
close(pipefd[1]);
close(errpipe[1]);
d->pid = pid;
d->errfd = errpipe[0];
pfd.fd = pipefd[0];
pfd.events = POLLIN|POLLHUP;
deadline = nowms() + Starttimeout;
n = 0;
while(n + 1 < (int)sizeof line){
pfd.revents = 0;
if(poll(&pfd, 1, leftms(deadline)) <= 0)
break;
nr = read(pipefd[0], line + n, sizeof line - n - 1);
if(nr < 0 && errno == EINTR)
continue;
if(nr <= 0)
break;
n += nr;
line[n] = '\0';
if(strchr(line, '\n') != NULL)
break;
}
close(pipefd[0]);
if(n == 0 || strchr(line, '\n') == NULL)
return fail("Xvfb did not report a private display");
line[strcspn(line, "\r\n")] = '\0';
if(strspn(line, "0123456789") != strlen(line))
return fail("invalid Xvfb display number %s", line);
if(snprintf(l->display, sizeof l->display, ":%s", line)
>= (int)sizeof l->display)
return fail("Xvfb display number is too long");
return 1;
}
int
startdaemon(Live *l, Daemon *d, char *program, char *arg)
{
int errpipe[2], fd;
long maxfd;
pid_t pid;
if(pipe2(errpipe, O_CLOEXEC|O_NONBLOCK) < 0)
return fail("pipe2: %s", strerror(errno));
pid = fork();
if(pid < 0){
close(errpipe[0]);
close(errpipe[1]);
return fail("fork: %s", strerror(errno));
}
if(pid == 0){
close(errpipe[0]);
if(!childfds(errpipe[1]))
_exit(126);
if(close_range(3, UINT_MAX, 0) < 0){
maxfd = sysconf(_SC_OPEN_MAX);
if(maxfd < 0)
maxfd = 1024;
for(fd = 3; fd < maxfd; fd++)
close(fd);
}
if(setenv("XDG_RUNTIME_DIR", l->runtime, 1) < 0 ||
setenv("XDG_CONFIG_HOME", l->config, 1) < 0 ||
setenv("HOME", l->home, 1) < 0 ||
(l->display[0] != '\0' ?
setenv("DISPLAY", l->display, 1) :
unsetenv("DISPLAY")) < 0 ||
unsetenv("DBUS_SESSION_BUS_ADDRESS") < 0 ||
unsetenv("IBUS_ADDRESS") < 0 ||
unsetenv("IBUS_ADDRESS_FILE") < 0){
dprintf(STDERR_FILENO, "set daemon environment: %s\n",
strerror(errno));
_exit(126);
}
execl(program, program, arg, (char*)0);
dprintf(STDERR_FILENO, "exec %s: %s\n", program, strerror(errno));
_exit(127);
}
close(errpipe[1]);
d->pid = pid;
d->errfd = errpipe[0];
return 1;
}
int
daemonalive(Daemon *d)
{
int n, status;
if(d->pid <= 0)
return fail("%s is no longer running", d->name);
do
n = waitpid(d->pid, &status, WNOHANG);
while(n < 0 && errno == EINTR);
if(n == 0)
return 1;
if(n == d->pid){
d->pid = -1;
return fail("%s exited unexpectedly with wait status %#x", d->name,
status);
}
if(n < 0){
fail("check %s %ld: %s", d->name, (long)d->pid, strerror(errno));
if(errno == ECHILD)
d->pid = -1;
return 0;
}
return fail("waitpid returned the wrong child for %s", d->name);
}
int
killdaemon(Daemon *d, int *status)
{
int n, ok, wait;
if(d->pid <= 0)
return 1;
ok = 1;
if(kill(d->pid, SIGKILL) < 0 && errno != ESRCH)
ok = fail("kill -9 %s %ld: %s", d->name, (long)d->pid,
strerror(errno));
do
n = waitpid(d->pid, &wait, 0);
while(n < 0 && errno == EINTR);
if(n != d->pid)
ok = fail("reap %s %ld after SIGKILL: %s", d->name, (long)d->pid,
n < 0 ? strerror(errno) : "wrong child");
else if(status != NULL)
*status = wait;
d->pid = -1;
return ok;
}
int
stopdaemon(Daemon *d)
{
struct pollfd pfd;
int n, ok, reaped, status;
int64_t deadline;
if(d->pid <= 0)
return 1;
if(!daemonalive(d))
return 0;
ok = 1;
status = 0;
reaped = 0;
if(kill(d->pid, SIGTERM) < 0 && errno != ESRCH)
ok = fail("kill %s %ld: %s", d->name, (long)d->pid, strerror(errno));
deadline = nowms() + Stoptimeout;
while(d->pid > 0){
do
n = waitpid(d->pid, &status, WNOHANG);
while(n < 0 && errno == EINTR);
if(n == d->pid){
reaped = 1;
d->pid = -1;
break;
}
if(n < 0){
fail("waitpid %s %ld: %s", d->name, (long)d->pid,
strerror(errno));
ok = 0;
if(errno == ECHILD)
d->pid = -1;
else if(!killdaemon(d, NULL))
ok = 0;
break;
}
n = leftms(deadline);
if(n == 0){
fail("%s %ld did not stop after SIGTERM", d->name, (long)d->pid);
ok = 0;
if(!killdaemon(d, NULL))
ok = 0;
break;
}
pfd.fd = d->errfd;
pfd.events = POLLIN|POLLHUP;
pfd.revents = 0;
if(poll(&pfd, 1, n) < 0 && errno != EINTR){
fail("poll %s %ld: %s", d->name, (long)d->pid, strerror(errno));
ok = 0;
if(!killdaemon(d, NULL))
ok = 0;
break;
}
readerrors(d);
}
/* plan9port turns a caught termination note into exit status 1. */
if(reaped && !((WIFSIGNALED(status) && WTERMSIG(status) == SIGTERM) ||
(WIFEXITED(status) && WEXITSTATUS(status) <= 1)))
ok = fail("%s exited with unexpected wait status %#x", d->name,
status);
readerrors(d);
return ok;
}
int
findaddress(Live *l)
{
DIR *dir;
struct dirent *de;
int count, ok;
dir = opendir(l->bus);
if(dir == NULL){
fail("open IBus directory %s: %s", l->bus, strerror(errno));
return -1;
}
count = 0;
ok = 1;
errno = 0;
while((de = readdir(dir)) != NULL){
if(strcmp(de->d_name, ".") == 0 || strcmp(de->d_name, "..") == 0 ||
strstr(de->d_name, ".tmp.") != NULL)
continue;
count++;
if(snprintf(l->addrfile, sizeof l->addrfile, "%s/%s", l->bus,
de->d_name) >= (int)sizeof l->addrfile){
ok = fail("IBus address path is too long");
break;
}
}
if(errno != 0)
ok = fail("read IBus directory %s: %s", l->bus, strerror(errno));
if(closedir(dir) < 0)
ok = fail("close IBus directory %s: %s", l->bus, strerror(errno));
if(!ok)
return -1;
if(count > 1){
fail("found %d IBus address files", count);
return -1;
}
return count;
}
int
readfile(char *path, char *buf, size_t cap, size_t *nread)
{
struct stat st;
ssize_t n;
size_t off;
char extra;
int fd, ok;
fd = open(path, O_RDONLY|O_CLOEXEC);
if(fd < 0)
return fail("open %s: %s", path, strerror(errno));
ok = 0;
if(fstat(fd, &st) < 0){
fail("stat open file %s: %s", path, strerror(errno));
goto out;
}
if(st.st_size < 0 || (uintmax_t)st.st_size >= cap){
fail("file %s is too large", path);
goto out;
}
off = 0;
while(off < (size_t)st.st_size){
n = read(fd, buf + off, (size_t)st.st_size - off);
if(n < 0 && errno == EINTR)
continue;
if(n <= 0){
fail("read %s: %s", path,
n == 0 ? "unexpected EOF" : strerror(errno));
goto out;
}
off += n;
}
do
n = read(fd, &extra, 1);
while(n < 0 && errno == EINTR);
if(n != 0){
fail("file %s changed while being read", path);
goto out;
}
buf[off] = '\0';
*nread = off;
ok = 1;
out:
if(close(fd) < 0){
fail("close %s: %s", path, strerror(errno));
ok = 0;
}
return ok;
}
int
parseaddress(char *contents, size_t ncontents, char *address, size_t naddress,
pid_t *declared)
{
char found[512];
long pid;
int consumed;
consumed = -1;
if(strlen(contents) != ncontents ||
sscanf(contents, "IBUS_ADDRESS=%511[^\n]\nIBUS_DAEMON_PID=%ld\n%n",
found, &pid, &consumed) != 2 || consumed != (int)ncontents || pid <= 0)
return fail("invalid IBus address file contents");
if(snprintf(address, naddress, "%s", found) >= (int)naddress)
return fail("private IBus address is too long");
*declared = (pid_t)pid;
return 1;
}
/*
* Wait for the protected IPC socket and, when addr is not null, for the
* IBus address file the daemon publishes for itself.
*/
int
waitready(Live *l, Daemon *d, char *addr, size_t naddr)
{
struct pollfd pfd;
struct stat st;
char contents[2048];
size_t ncontents;
pid_t declared;
int count, n, timeout;
int64_t deadline;
deadline = nowms() + Starttimeout;
for(;;){
if(lstat(l->socket, &st) == 0 && S_ISSOCK(st.st_mode) &&
(st.st_mode & 0777) == 0600){
if(addr == NULL)
return 1;
count = findaddress(l);
if(count < 0)
return 0;
if(count == 1 &&
readfile(l->addrfile, contents, sizeof contents, &ncontents) &&
parseaddress(contents, ncontents, addr, naddr, &declared) &&
declared == d->pid)
return 1;
}
if(!daemonalive(d))
return 0;
timeout = leftms(deadline);
if(timeout == 0)
return fail("timed out waiting for the %s endpoints", d->name);
pfd.fd = d->errfd;
pfd.events = POLLIN|POLLHUP;
pfd.revents = 0;
n = poll(&pfd, 1, timeout > 20 ? 20 : timeout);
if(n < 0 && errno == EINTR)
continue;
if(n < 0)
return fail("poll %s readiness: %s", d->name, strerror(errno));
if(pfd.revents & (POLLERR|POLLNVAL))
return fail("%s stderr pipe became unusable: %#x", d->name,
pfd.revents);
if(pfd.revents & (POLLIN|POLLHUP))
readerrors(d);
}
}
int
connectsocket(char *path, int64_t deadline)
{
struct sockaddr_un addr;
struct pollfd pfd;
socklen_t nerr;
int err, fd, flags, n;
memset(&addr, 0, sizeof addr);
addr.sun_family = AF_UNIX;
if(snprintf(addr.sun_path, sizeof addr.sun_path, "%s", path)
>= (int)sizeof addr.sun_path){
errno = ENAMETOOLONG;
return -1;
}
fd = socket(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0);
if(fd < 0)
return -1;
if(connect(fd, (struct sockaddr*)&addr, sizeof addr) < 0 &&
errno != EINPROGRESS){
err = errno;
close(fd);
errno = err;
return -1;
}
for(;;){
n = leftms(deadline);
if(n == 0){
close(fd);
errno = ETIMEDOUT;
return -1;
}
pfd.fd = fd;
pfd.events = POLLOUT;
pfd.revents = 0;
n = poll(&pfd, 1, n);
if(n < 0 && errno == EINTR)
continue;
if(n <= 0){
err = n == 0 ? ETIMEDOUT : errno;
close(fd);
errno = err;
return -1;
}
break;
}
err = 0;
nerr = sizeof err;
if(getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &nerr) < 0 || err != 0){
if(err == 0)
err = errno;
close(fd);
errno = err;
return -1;
}
flags = fcntl(fd, F_GETFL);
if(flags < 0 || fcntl(fd, F_SETFL, flags & ~O_NONBLOCK) < 0){
err = errno;
close(fd);
errno = err;
return -1;
}
return fd;
}
/* 1 is complete, 0 is peer closure, and -1 is a timeout or I/O error. */
int
readuntil(int fd, void *buf, size_t n, int64_t deadline)
{
struct pollfd pfd;
unsigned char *p;
ssize_t r;
int timeout;
p = buf;
while(n > 0){
timeout = leftms(deadline);
if(timeout == 0){
errno = ETIMEDOUT;
return -1;
}
pfd.fd = fd;
pfd.events = POLLIN;
pfd.revents = 0;
r = poll(&pfd, 1, timeout);
if(r < 0 && errno == EINTR)
continue;
if(r <= 0){
if(r == 0)
errno = ETIMEDOUT;
return -1;
}
r = recv(fd, p, n, 0);
if(r < 0 && errno == EINTR)
continue;
if(r < 0)
return -1;
if(r == 0)
return 0;
p += r;
n -= r;
}
return 1;
}
int
ipcrequest(int fd, uint32_t mod, uint32_t key, unsigned char *want,
size_t nwant, char *where)
{
unsigned char req[Ipcreqsz], got[16];
int rv;
if(nwant > sizeof got)
return fail("%s expected IPC response is too large", where);
ipcpackreq(req, 1, mod, key);
if(ipcsend(fd, req, sizeof req) < 0)
return fail("send %s IPC request: %s", where, strerror(errno));
rv = readuntil(fd, got, nwant, nowms() + Calltimeout);
if(rv != 1)
return fail("read %s IPC response: %s", where,
rv == 0 ? "peer closed" : strerror(errno));
if(memcmp(got, want, nwant) != 0)
return fail("%s IPC response did not match", where);
return 1;
}
int
ipcprobe(int fd, char *where)
{
/* eaten, take-back, no commit, no preedit */
unsigned char empty[] = {0, 0, 0, 0, 0, 0};
return ipcrequest(fd, 0, 0, empty, sizeof empty, where);
}

View File

@@ -1,75 +0,0 @@
#include <stddef.h>
#include <stdint.h>
#include <sys/types.h>
/*
* Harness shared by the live tests: a strans daemon started in a private
* XDG_RUNTIME_DIR with its output captured, the endpoints it publishes,
* and the IPC probe used to prove the daemon answers.
* Set testname before anything else; fail prefixes it to every message.
*/
typedef struct Daemon Daemon;
typedef struct Live Live;
enum
{
Calltimeout = 4000,
Starttimeout = 8000,
Stoptimeout = 3000,
Errmax = 8192,
};
struct Daemon
{
pid_t pid;
int errfd;
char *name;
char err[Errmax];
size_t nerr;
};
struct Live
{
char root[256];
char runtime[320];
char config[320];
char ibus[384];
char bus[448];
char home[320];
char socket[384];
char addrfile[512];
char display[32];
};
extern char *testname;
int fail(char*, ...);
int64_t nowms(void);
int leftms(int64_t);
void pausems(int);
int makedir(char*);
int cleardir(char*);
int livesetup(Live*, char*);
int liveclean(Live*);
void daemoninit(Daemon*, char*);
int startxvfb(Live*, Daemon*, char*);
int startdaemon(Live*, Daemon*, char*, char*);
int waitready(Live*, Daemon*, char*, size_t);
int daemonalive(Daemon*);
int stopdaemon(Daemon*);
int killdaemon(Daemon*, int*);
void readerrors(Daemon*);
void showerrors(Daemon*);
int closeerrors(Daemon*);
int findaddress(Live*);
int readfile(char*, char*, size_t, size_t*);
int parseaddress(char*, size_t, char*, size_t, pid_t*);
int connectsocket(char*, int64_t);
int readuntil(int, void*, size_t, int64_t);
int ipcrequest(int, uint32_t, uint32_t, unsigned char*, size_t, char*);
int ipcprobe(int, char*);

View File

@@ -1,168 +0,0 @@
#define _GNU_SOURCE
#include <dbus/dbus.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include "live.h"
#include "livebus.h"
DBusConnection*
openbus(char *address, char *where)
{
DBusConnection *conn;
DBusError err;
dbus_error_init(&err);
conn = dbus_connection_open_private(address, &err);
if(conn == NULL){
fail("open %s private IBus connection: %s", where,
err.message != NULL ? err.message : "D-Bus error");
dbus_error_free(&err);
return NULL;
}
dbus_error_free(&err);
dbus_connection_set_exit_on_disconnect(conn, FALSE);
return conn;
}
void
closebus(DBusConnection **conn)
{
if(*conn == NULL)
return;
dbus_connection_close(*conn);
dbus_connection_unref(*conn);
*conn = NULL;
}
DBusMessage*
contextcall(char *path, char *member)
{
return dbus_message_new_method_call("org.freedesktop.IBus", path,
"org.freedesktop.IBus.InputContext", member);
}
DBusMessage*
sendcall(DBusConnection *conn, DBusMessage *m, char *where)
{
DBusMessage *reply;
DBusPendingCall *pending;
int timeout;
int64_t deadline;
if(m == NULL){
fail("allocate %s call", where);
return NULL;
}
pending = NULL;
if(!dbus_connection_send_with_reply(conn, m, &pending, Calltimeout) ||
pending == NULL){
dbus_message_unref(m);
fail("queue %s call", where);
return NULL;
}
dbus_message_unref(m);
deadline = nowms() + Calltimeout;
while(!dbus_pending_call_get_completed(pending)){
timeout = leftms(deadline);
if(timeout == 0){
fail("timed out waiting for %s", where);
goto out;
}
if(!dbus_connection_read_write_dispatch(conn, timeout)){
fail("connection closed waiting for %s", where);
goto out;
}
}
reply = dbus_pending_call_steal_reply(pending);
dbus_pending_call_unref(pending);
if(reply == NULL)
fail("%s completed without a reply", where);
return reply;
out:
dbus_pending_call_cancel(pending);
dbus_pending_call_unref(pending);
return NULL;
}
DBusMessage*
callret(DBusConnection *conn, DBusMessage *m, char *where)
{
DBusMessage *reply;
reply = sendcall(conn, m, where);
if(reply == NULL)
return NULL;
if(dbus_message_get_type(reply) != DBUS_MESSAGE_TYPE_METHOD_RETURN){
fail("%s returned D-Bus message type %d", where,
dbus_message_get_type(reply));
dbus_message_unref(reply);
return NULL;
}
return reply;
}
int
hello(DBusConnection *conn, char *name, size_t nname, char *where)
{
DBusMessage *m, *reply;
DBusError err;
const char *s;
int ok;
m = dbus_message_new_method_call("org.freedesktop.DBus",
"/org/freedesktop/DBus", "org.freedesktop.DBus", "Hello");
reply = callret(conn, m, where);
if(reply == NULL)
return 0;
dbus_error_init(&err);
ok = dbus_message_has_signature(reply, "s") &&
dbus_message_get_args(reply, &err, DBUS_TYPE_STRING, &s,
DBUS_TYPE_INVALID);
if(!ok)
fail("%s returned an invalid Hello reply", where);
else if(s[0] != ':')
ok = fail("%s returned invalid Hello name %s", where, s);
else if(name != NULL && snprintf(name, nname, "%s", s) >= (int)nname)
ok = fail("%s Hello name is too long", where);
dbus_error_free(&err);
dbus_message_unref(reply);
return ok;
}
int
createcontext(DBusConnection *conn, char *path, size_t npath, char *where)
{
DBusMessage *m, *reply;
DBusError err;
const char *client, *p;
int ok;
client = testname;
m = dbus_message_new_method_call("org.freedesktop.IBus",
"/org/freedesktop/IBus", "org.freedesktop.IBus",
"CreateInputContext");
if(m == NULL || !dbus_message_append_args(m, DBUS_TYPE_STRING, &client,
DBUS_TYPE_INVALID)){
if(m != NULL)
dbus_message_unref(m);
return fail("build %s CreateInputContext call", where);
}
reply = callret(conn, m, where);
if(reply == NULL)
return 0;
dbus_error_init(&err);
ok = dbus_message_has_signature(reply, "o") &&
dbus_message_get_args(reply, &err, DBUS_TYPE_OBJECT_PATH, &p,
DBUS_TYPE_INVALID);
if(!ok)
fail("%s returned an invalid context reply", where);
else if(p[0] != '/')
ok = fail("%s returned invalid context path %s", where, p);
else if(path != NULL && snprintf(path, npath, "%s", p) >= (int)npath)
ok = fail("%s context path is too long", where);
dbus_error_free(&err);
dbus_message_unref(reply);
return ok;
}

View File

@@ -1,17 +0,0 @@
#include <stddef.h>
#include <dbus/dbus.h>
/*
* D-Bus half of the live harness: private connections to the IBus address
* a strans daemon publishes, and the calls every IBus client makes first.
* sendcall returns any reply, callret only a method return.
*/
DBusConnection* openbus(char*, char*);
void closebus(DBusConnection**);
DBusMessage* contextcall(char*, char*);
DBusMessage* sendcall(DBusConnection*, DBusMessage*, char*);
DBusMessage* callret(DBusConnection*, DBusMessage*, char*);
int hello(DBusConnection*, char*, size_t, char*);
int createcontext(DBusConnection*, char*, size_t, char*);

View File

@@ -1,91 +0,0 @@
#!/usr/bin/env python3
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
MKEMOJI = ROOT / "map" / "mkemoji"
TIMEOUT = 10
def generate(*sources):
args = [sys.executable, "-B", str(MKEMOJI)] + [str(s) for s in sources]
return subprocess.run(
args, capture_output=True, text=True, check=False, timeout=TIMEOUT
)
def table(output):
return dict(line.split("\t", 1) for line in output.splitlines())
class MkemojiTest(unittest.TestCase):
def source(self, text):
tmp = tempfile.TemporaryDirectory()
path = Path(tmp.name) / "emoji.src"
path.write_text(text, encoding="utf-8")
self.addCleanup(tmp.cleanup)
return path
def test_production_digit_slots(self):
result = generate()
self.assertEqual(result.returncode, 0, result.stderr)
data = table(result.stdout)
# The engine walks a prefix's children in rune order, so only the
# prefix's own row decides which candidate a digit picks.
self.assertEqual(data["^"].split(), list("¹²³⁴⁵⁶⁷⁸⁹"))
self.assertEqual(data["_"].split(), list("₁₂₃₄₅₆₇₈₉"))
self.assertEqual(data["<"].split()[2], "")
self.assertEqual(data["^1"], "¹")
self.assertEqual(data["_2"], "")
self.assertEqual(data["<3"].split()[0], "")
self.assertIn("😀", data["smile"].split())
self.assertIn("😀", data["웃음"].split())
def test_fold_normalize_and_source_order(self):
source = self.source(
"β\tALPHABET\talpha\n"
"α\talpha\n"
"\t\n"
"#\thash\n"
)
first = generate(source)
second = generate(source, source)
self.assertEqual(first.returncode, 0, first.stderr)
self.assertEqual(first.stdout, second.stdout)
data = table(first.stdout)
self.assertEqual(list(data), ["alphabet", "alpha", "é", "hash"])
self.assertEqual(data["alpha"].split(), ["β", "α"])
self.assertEqual(data["é"], "é")
self.assertEqual(data["hash"], "#")
def test_rejects_malformed_and_oversized_rows(self):
bad = [
"x\n",
"bad result\ta\n",
"x\t\n",
"x\t a\n",
"\0\ta\n",
"x\ta\0b\n",
"x\t;hidden\n",
"x" * 65 + "\ta\n",
"x\t" + "a" * 65 + "\n",
]
for text in bad:
with self.subTest(text=repr(text)):
result = generate(self.source(text))
self.assertEqual(result.returncode, 1)
def test_accepts_runtime_boundary(self):
source = self.source("x" * 64 + "\t" + "a" * 64 + "\n")
result = generate(source)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertIn("a" * 64 + "\t" + "x" * 64, result.stdout)
if __name__ == "__main__":
unittest.main()

View File

@@ -1,190 +0,0 @@
#!/usr/bin/env python3
import os
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
IMPORT = ROOT / "map" / "libhangul2hanja"
GENERATE = ROOT / "map" / "mkhanja"
TIMEOUT = 10
def run(program, source=None, stdin=None, hostile=False):
command = [sys.executable, "-B", str(program)]
if source is not None:
command.append(str(source))
env = None
if hostile:
env = os.environ.copy()
env.update(LC_ALL="C", LANG="C", PYTHONCOERCECLOCALE="0", PYTHONUTF8="0")
env.pop("PYTHONIOENCODING", None)
return subprocess.run(
command,
input=stdin,
capture_output=True,
encoding="utf-8",
env=env,
check=False,
timeout=TIMEOUT,
)
class MkhanjaTest(unittest.TestCase):
def source(self, text):
tmp = tempfile.TemporaryDirectory()
path = Path(tmp.name) / "hanja.txt"
path.write_text(text, encoding="utf-8")
self.addCleanup(tmp.cleanup)
return path
def test_imports_bmp_hanja_rows(self):
result = run(
IMPORT,
self.source(
"# Copyright holder\n"
"# BSD license\n"
"\ud55c:\u6f22:first\n"
"\uac00:\u3400:extension A\n"
"\uae40:\u91d1:compatibility\n"
"\ud55c\uc790:\u6f22\u5b57:word\n"
"\ud55c:\U00020000:astral\n"
"\u3131:\u52a0:jamo\n"
"\ud55c\uae00:\u97d3glyph:mixed\n"
),
)
self.assertEqual(result.returncode, 0, result.stderr)
lines = result.stdout.splitlines()
self.assertEqual(lines[:2], ["# Copyright holder", "# BSD license"])
self.assertEqual(lines[2:], ["", "\u6f22\t\ud55c", "\u3400\t\uac00",
"\u91d1\t\uae40",
"\u6f22\u5b57\t\ud55c\uc790"])
def test_imports_symbol_rows_under_a_jamo(self):
result = run(
IMPORT,
self.source(
"\u3141:\u203b:reference mark\n"
"\u3134:\u300c:bracket\n"
"\u3131:\u3000:ideographic space\n"
"\u3131:\u00ad:soft hyphen\n"
"\u3131:\u52a0:Hanja under a jamo\n"
"\u3141:\u203b\u203b:two runes\n"
"\u3141\u3134:\u203b:two jamo\n"
"\u314f:\u203b:a vowel\n"
),
)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(result.stdout.splitlines(),
["\u203b\t\u3141", "\u300c\t\u3134"])
def test_import_rejects_malformed_and_duplicate_rows(self):
bad = [
"한 漢\n",
"한:漢:first\n한:漢:again\n",
]
for text in bad:
with self.subTest(text=repr(text)):
self.assertEqual(run(IMPORT, self.source(text)).returncode, 1)
def test_import_uses_utf8_without_locale(self):
text = "# 저작권\n한:漢:first\n"
expected = "# 저작권\n\n\t\n"
for source, stdin in ((self.source(text), None), (None, text)):
with self.subTest(source=source):
result = run(IMPORT, source, stdin, hostile=True)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(result.stdout, expected)
def test_groups_readings_and_preserves_order(self):
result = run(
GENERATE,
self.source(
"# Copyright holder\n"
"# BSD license\n"
"\t\n"
"\t\n"
"\t\n"
"\t\n"
"\t\n"
"漢字\t한자\n"
),
)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(
result.stdout.splitlines(),
[
";; Copyright holder",
";; BSD license",
"",
"\t漢 韓",
"\t㐀 家",
"\t",
"한자\t漢字",
],
)
def test_groups_symbols_under_their_jamo(self):
result = run(
GENERATE,
self.source(
"\t\n"
"\t\n"
"\t\n"
),
)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(result.stdout.splitlines(),
["\t※ ○", "\t"])
def test_generator_rejects_bad_rows(self):
bad = [
"漢a\t\n",
"\t한a\n",
"𠀀\t\n",
"\t\n\t\n",
"漢 한\n",
"\t\n",
"\t\n",
"\tㅁㄴ\n",
"※※\t\n",
"\u3000\t\n",
"\u00ad\t\n",
]
for text in bad:
with self.subTest(text=repr(text)):
self.assertEqual(run(GENERATE, self.source(text)).returncode, 1)
def test_generator_uses_utf8_without_locale(self):
result = run(GENERATE, self.source("# 저작권\n\t\n"), hostile=True)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(result.stdout, ";; 저작권\n\n\t\n")
result = run(GENERATE, hostile=True)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertIn("\t", result.stdout)
def test_generator_reads_both_sources(self):
result = run(GENERATE)
self.assertEqual(result.returncode, 0, result.stderr)
lines = result.stdout.splitlines()
self.assertEqual(lines.count(";; All rights reserved."), 2)
rows = dict(line.split("\t") for line in lines if "\t" in line)
self.assertIn("", rows[""].split())
self.assertIn("", rows[""].split())
def test_generator_keeps_runtime_candidate_limit(self):
rows = "".join(f"{chr(0x4E00 + n)}\t\n" for n in range(129))
result = run(GENERATE, self.source(rows))
self.assertEqual(result.returncode, 0, result.stderr)
candidates = result.stdout.rstrip().split("\t", 1)[1].split()
self.assertEqual(len(candidates), 128)
self.assertEqual(candidates[-1], chr(0x4E00 + 127))
if __name__ == "__main__":
unittest.main()

View File

@@ -1,323 +0,0 @@
#include "dat.h"
#include "fn.h"
#include "test.h"
/* A popup with every section, tall enough for the tests. */
#define Imgh (2*PopupPad + (Maxdisp + 2)*Fontsz + PopupSep)
enum {
Rgbmask = 0xffffff,
};
static u32int
pixel(u32int *img, Popup *p, int x, int y)
{
return img[y*p->w + x] & Rgbmask;
}
static int
rectcolor(u32int *img, Popup *p, int x0, int y0, int x1, int y1,
u32int color)
{
int x, y;
color &= Rgbmask;
for(y = y0; y < y1; y++)
for(x = x0; x < x1; x++)
if(pixel(img, p, x, y) != color)
return 0;
return 1;
}
static void
checkpadding(struct ct *t, u32int *img, Popup *p)
{
CT_CHECK(t, rectcolor(img, p, 0, 0, p->w, PopupPad, Colbg));
CT_CHECK(t, rectcolor(img, p, 0, p->h - PopupPad,
p->w, p->h, Colbg));
CT_CHECK(t, rectcolor(img, p, 0, 0, PopupPad, p->h, Colbg));
CT_CHECK(t, rectcolor(img, p, p->w - PopupPad, 0,
p->w, p->h, Colbg));
}
void
popup_layout(struct ct *t)
{
static const struct {
int first;
int shown;
int total;
char *want;
} markers[] = {
{ 0, 0, 0, "" },
{ 0, 9, 9, "" },
{ 0, 3, 9, "1-3/9" },
{ 8, 1, 9, "9-9/9" },
{ 0, 9, 10, "1-9/10" },
{ 9, 1, 10, "10-10/10" },
{ 0, 9, 18, "1-9/18" },
{ 9, 9, 18, "10-18/18" },
{ 9, 9, 32, "10-18/32" },
{ 18, 9, 32, "19-27/32" },
{ 27, 5, 32, "28-32/32" },
};
char buf[32];
Area area, mon[2], out, work;
Caret caret;
Drawcmd dc;
Popup p;
Str longrow;
u32int c, *img;
int i, ink, markink, n, numink, selbg, textink, x, y;
for(i = 0; i < nelem(markers); i++){
n = pagemarker(buf, sizeof buf, markers[i].first,
markers[i].shown, markers[i].total);
CT_EQ_INT(t, strlen(markers[i].want), n);
CT_CHECK(t, strcmp(markers[i].want, buf) == 0);
}
/* Monitor selection retains origins and intersects the EWMH work area. */
mon[0] = (Area){0, 0, 1920, 1080};
mon[1] = (Area){1920, 200, 1280, 1024};
work = (Area){0, 30, 3200, 1070};
popuparea(mon, 2, &work, 2500, 500, &out);
CT_EQ_INT(t, 1920, out.x);
CT_EQ_INT(t, 200, out.y);
CT_EQ_INT(t, 1280, out.w);
CT_EQ_INT(t, 900, out.h);
work = (Area){60, 0, 3140, 1224};
popuparea(mon, 2, &work, 100, 100, &out);
CT_EQ_INT(t, 60, out.x);
CT_EQ_INT(t, 0, out.y);
CT_EQ_INT(t, 1860, out.w);
CT_EQ_INT(t, 1080, out.h);
work = (Area){5000, 0, 100, 100};
popuparea(mon, 2, &work, 2500, 500, &out);
CT_EQ_INT(t, mon[1].x, out.x);
CT_EQ_INT(t, mon[1].y, out.y);
CT_EQ_INT(t, mon[1].w, out.w);
CT_EQ_INT(t, mon[1].h, out.h);
mon[0] = (Area){0, 0, 100, 100};
mon[1] = (Area){200, 0, 100, 100};
popuparea(mon, 2, nil, 150, 50, &out);
CT_EQ_INT(t, 0, out.x);
memset(&caret, 0, sizeof caret);
area = (Area){1920, 30, 1280, 900};
popupposition(&caret, 2000, 50, &area, 100, 60, &x, &y);
CT_EQ_INT(t, 2010, x);
CT_EQ_INT(t, 60, y);
popupposition(&caret, 2000, 900, &area, 100, 60, &x, &y);
CT_EQ_INT(t, 830, y); /* above the pointer, not over it */
caret.valid = 1;
caret.x = 2100;
caret.y = 80;
caret.h = 20;
popupposition(&caret, 0, 0, &area, 100, 60, &x, &y);
CT_EQ_INT(t, 2100, x);
CT_EQ_INT(t, 100, y);
caret.x = 3150;
caret.y = 850;
popupposition(&caret, 0, 0, &area, 200, 100, &x, &y);
CT_EQ_INT(t, 3000, x);
CT_EQ_INT(t, 750, y);
/* A popup no larger than a small work area stays inside its origin. */
area = (Area){100, 200, 80, 80};
caret.x = 120;
caret.y = 230;
caret.h = 20;
popupposition(&caret, 0, 0, &area, 80, 80, &x, &y);
CT_EQ_INT(t, 100, x);
CT_EQ_INT(t, 200, y);
textinit();
/* Empty sections reserve nothing. */
memset(&dc, 0, sizeof dc);
dc.sel = -1;
popuplayout(&dc, PopupBasew, Imgh, &p);
CT_EQ_INT(t, 0, p.w);
CT_EQ_INT(t, 0, p.h);
CT_EQ_INT(t, -1, p.prey);
CT_EQ_INT(t, -1, p.rowsy);
CT_EQ_INT(t, -1, p.marky);
/* Preedit only: as wide as its text. */
dc.pre = mkstr("preedit");
popuplayout(&dc, 2*PopupBasew, Imgh, &p);
CT_EQ_INT(t, textwidth(&dc.pre) + 2*PopupPad, p.w);
CT_EQ_INT(t, PopupPad, p.prey);
CT_EQ_INT(t, -1, p.sepy);
CT_EQ_INT(t, -1, p.rowsy);
CT_EQ_INT(t, -1, p.marky);
CT_EQ_INT(t, 2*PopupPad + Fontsz, p.h);
/* Candidates only. */
memset(&dc, 0, sizeof dc);
dc.sel = -1;
dc.nkouho = 2;
dc.kouho[0] = mkstr("short");
dc.kouho[1] = mkstr("candidate");
popuplayout(&dc, 2*PopupBasew, Imgh, &p);
CT_EQ_INT(t, PopupBasew, p.w);
CT_EQ_INT(t, -1, p.prey);
CT_EQ_INT(t, -1, p.sepy);
CT_EQ_INT(t, PopupPad, p.rowsy);
CT_EQ_INT(t, -1, p.marky);
CT_EQ_INT(t, 2*PopupPad + 2*Fontsz, p.h);
CT_EQ_INT(t, PopupTextw, p.textw);
CT_EQ_INT(t, p.w - PopupPad, PopupPad + PopupNumw + p.textw);
/* Preedit and candidates meet at one separator, with no empty rows. */
dc.pre = mkstr("preedit");
popuplayout(&dc, 2*PopupBasew, Imgh, &p);
CT_EQ_INT(t, PopupBasew, p.w);
CT_EQ_INT(t, PopupPad, p.prey);
CT_EQ_INT(t, p.prey + Fontsz, p.sepy);
CT_EQ_INT(t, p.sepy + PopupSep, p.rowsy);
CT_EQ_INT(t, -1, p.marky);
CT_EQ_INT(t, p.rowsy + 2*Fontsz + PopupPad, p.h);
CT_EQ_INT(t, 2*PopupPad + 3*Fontsz + PopupSep, p.h);
/* Long preedit expands to the monitor cap just like a candidate. */
sclear(&longrow);
for(i = 0; i < Maxrunes; i++)
sputr(&longrow, 'W');
memset(&dc, 0, sizeof dc);
dc.pre = longrow;
popuplayout(&dc, 2*PopupBasew, Imgh, &p);
CT_EQ_INT(t, 2*PopupBasew, p.w);
/* The largest panel includes all sections and fits its backing image. */
memset(&dc, 0, sizeof dc);
dc.pre = mkstr("preedit");
dc.nkouho = Maxdisp;
dc.sel = 4;
dc.first = 0;
dc.total = Maxdisp + 1;
for(i = 0; i < Maxdisp; i++)
dc.kouho[i] = mkstr(i == dc.sel ? "M" : "candidate");
dc.kouho[0] = longrow;
popuplayout(&dc, 2*PopupBasew, Imgh, &p);
CT_EQ_INT(t, Maxdisp, p.n);
CT_EQ_INT(t, PopupPad, p.prey);
CT_EQ_INT(t, p.prey + Fontsz, p.sepy);
CT_EQ_INT(t, p.sepy + PopupSep, p.rowsy);
CT_EQ_INT(t, p.rowsy + Maxdisp*Fontsz, p.marky);
CT_EQ_INT(t, p.marky + Fontsz + PopupPad, p.h);
CT_EQ_INT(t, Imgh, p.h);
CT_EQ_INT(t, 2*PopupBasew, p.w);
CT_CHECK(t, p.h <= Imgh);
CT_EQ_INT(t, p.w - PopupPad, p.markx + p.markw);
CT_EQ_INT(t, p.w - 2*PopupPad, p.selw);
CT_EQ_INT(t, p.rowsy + dc.sel*Fontsz, p.sely);
CT_EQ_INT(t, p.w - PopupPad, PopupPad + PopupNumw + p.textw);
img = emalloc(2*PopupBasew*Imgh*sizeof img[0]);
popupdraw(img, &dc, &p);
checkpadding(t, img, &p);
for(x = 0; x < p.w; x++)
CT_EQ_UINT(t,
x >= PopupPad && x < p.w - PopupPad ? Colsep : Colbg,
pixel(img, &p, x, p.sepy));
/* Selection paint and both foreground columns stay inside the row. */
selbg = numink = textink = 0;
for(y = p.sely; y < p.sely + Fontsz; y++){
for(x = 0; x < p.w; x++){
c = pixel(img, &p, x, y);
if(x < PopupPad || x >= PopupPad + p.selw){
CT_EQ_UINT(t, Colbg, c);
continue;
}
if(c == (Colsel & Rgbmask)){
selbg++;
continue;
}
CT_CHECK(t,
(x >= PopupPad && x < PopupPad + PopupNumw) ||
(x >= (PopupPad + PopupNumw) && x < (PopupPad + PopupNumw) + p.textw));
if(x < PopupPad + PopupNumw)
numink++;
else
textink++;
}
}
CT_CHECK(t, Colsel != Colselfg);
CT_CHECK(t, selbg > p.selw*Fontsz/2);
CT_CHECK(t, numink > 0);
CT_CHECK(t, textink > 0);
/* The marker occupies only its compact, right-aligned footer box. */
markink = 0;
for(y = p.marky; y < p.marky + Fontsz; y++){
for(x = 0; x < p.w; x++){
if(pixel(img, &p, x, y) == (Colbg & Rgbmask))
continue;
markink++;
CT_CHECK(t, x >= p.markx && x < p.markx + p.markw);
}
}
CT_CHECK(t, markink > 0);
/* A long candidate grows from the stable base to the monitor cap. */
memset(&dc, 0, sizeof dc);
dc.sel = -1;
dc.nkouho = 1;
dc.kouho[0] = longrow;
n = textwidth(&longrow);
popuplayout(&dc, 2*PopupBasew, Imgh, &p);
CT_EQ_INT(t, 2*PopupBasew, p.w);
CT_CHECK(t, p.textw > PopupTextw);
CT_CHECK(t, n > p.textw);
CT_EQ_INT(t, PopupPad, p.rowsy);
CT_EQ_INT(t, p.w - PopupPad, PopupPad + PopupNumw + p.textw);
popupdraw(img, &dc, &p);
ink = 0;
for(y = p.rowsy; y < p.rowsy + Fontsz; y++){
for(x = 0; x < p.w; x++){
c = pixel(img, &p, x, y);
if(x < PopupPad || x >= p.w - PopupPad)
CT_EQ_UINT(t, Colbg, c);
if(x >= (PopupPad + PopupNumw) && x < (PopupPad + PopupNumw) + p.textw &&
c != (Colbg & Rgbmask))
ink++;
}
}
CT_CHECK(t, ink > 0);
CT_EQ_INT(t, n, textwidth(&longrow));
popuplayout(&dc, PopupBasew - 1, Imgh, &p);
CT_EQ_INT(t, PopupBasew - 1, p.w);
/* Height truncation scrolls the row window to the selected candidate. */
memset(&dc, 0, sizeof dc);
dc.nkouho = Maxdisp;
dc.sel = Maxdisp - 1;
dc.total = Maxdisp;
for(i = 0; i < Maxdisp; i++)
dc.kouho[i] = mkstr("candidate");
popuplayout(&dc, PopupBasew, 2*PopupPad + 2*Fontsz, &p);
CT_EQ_INT(t, 1, p.n);
CT_EQ_INT(t, Maxdisp - 1, p.row0);
CT_EQ_INT(t, PopupPad, p.sely);
CT_CHECK(t, p.marky >= 0);
CT_CHECK(t, p.h <= 2*PopupPad + 2*Fontsz);
popupdraw(img, &dc, &p);
numink = 0;
for(y = p.rowsy; y < p.rowsy + Fontsz; y++)
for(x = PopupPad; x < PopupPad + PopupNumw; x++)
if(pixel(img, &p, x, y) != (Colsel & Rgbmask))
numink++;
markink = 0;
for(y = p.marky; y < p.marky + Fontsz; y++)
for(x = p.markx; x < p.markx + p.markw; x++)
if(pixel(img, &p, x, y) != (Colbg & Rgbmask))
markink++;
CT_CHECK(t, numink > 0); /* full-width */
CT_CHECK(t, markink > 0); /* 9-9/9 */
free(img);
}

View File

@@ -1,549 +0,0 @@
#include <errno.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <unistd.h>
#include "srv.c"
#include "test.h"
#undef recv
typedef struct Serverfix Serverfix;
typedef struct Testclient Testclient;
/* A held pump plus a private client-slot channel of the wanted depth. */
struct Serverfix
{
Pump pump;
Channel *oldclientc;
};
struct Testclient
{
int fd;
int peer;
dev_t dev;
ino_t ino;
Channel *done;
};
static void
serverbegin(Serverfix *f, int nclients)
{
Drawcmd dc;
uchar token;
f->oldclientc = clientc;
while(channbrecv(drawc, &dc) > 0)
;
clientc = chancreate(sizeof token, nclients);
testengineinit(LangJP);
pumpstart(&f->pump, 0);
pumphold(&f->pump, Pumpall);
}
static void
serverend(Serverfix *f)
{
Drawcmd dc;
pumpstop(&f->pump);
chanfree(clientc);
clientc = f->oldclientc;
while(channbrecv(drawc, &dc) > 0)
;
}
static void
clientproc(void *arg)
{
Testclient *client;
uchar token;
client = arg;
clientthread((void*)(uintptr)client->fd);
token = 0;
chansend(client->done, &token);
}
static int
startclient(struct ct *t, Testclient *client)
{
struct stat st;
int fd[2];
uchar token;
memset(client, 0, sizeof *client);
client->fd = -1;
client->peer = -1;
if(socketpair(AF_UNIX, SOCK_STREAM, 0, fd) < 0)
return CT_ERRORF(t, "socketpair failed: %s", strerror(errno));
client->fd = fd[0];
client->peer = fd[1];
if(fstat(client->fd, &st) < 0){
close(client->fd);
close(client->peer);
client->fd = -1;
client->peer = -1;
return CT_ERRORF(t, "socket stat failed: %s", strerror(errno));
}
client->dev = st.st_dev;
client->ino = st.st_ino;
client->done = chancreate(sizeof token, 0);
token = 0;
if(channbsend(clientc, &token) <= 0){
close(client->fd);
close(client->peer);
chanfree(client->done);
client->fd = -1;
client->peer = -1;
client->done = nil;
return CT_ERRORF(t, "client slot unavailable");
}
if(proccreate(clientproc, client, 8192) < 0){
chanrecv(clientc, &token);
close(client->fd);
close(client->peer);
chanfree(client->done);
client->fd = -1;
client->peer = -1;
client->done = nil;
return CT_ERRORF(t, "client worker creation failed");
}
return 1;
}
static int
sendkey(struct ct *t, Testclient *client, int want, u32int mod, u32int key)
{
uchar req[Ipcreqsz];
ipcpackreq(req, want, mod, key);
if(ipcsend(client->peer, req, sizeof req) == 0)
return 1;
return CT_ERRORF(t, "request send failed: %s", strerror(errno));
}
static int
sendreset(struct ct *t, Testclient *client, int want)
{
uchar req[Ipcreqsz];
ipcpackreset(req, want);
if(ipcsend(client->peer, req, sizeof req) == 0)
return 1;
return CT_ERRORF(t, "reset send failed: %s", strerror(errno));
}
static Keyreq
nextrequestcap(struct ct *t, Pump *p, int op, int cap)
{
Keyreq req;
memset(&req, 0, sizeof req);
chanrecv(p->trace, &req);
CT_EQ_INT(t, op, req.op);
CT_EQ_INT(t, cap, req.clientpre);
return req;
}
static Keyreq
nextrequest(struct ct *t, Pump *p, int op)
{
return nextrequestcap(t, p, op, 1);
}
static void
allowrequest(Pump *p)
{
uchar token;
token = 0;
chansend(p->go, &token);
}
static int
readreply(struct ct *t, Testclient *client, int want, char *wantcommit,
char *preedit)
{
char commit[Ipcfieldmax+1];
Ipcresp resp;
if(ipcreadresp(client->peer, want, commit, preedit, &resp) < 0)
return CT_ERRORF(t, "response read failed: %s", strerror(errno));
CT_EQ_STR(t, wantcommit, commit);
return resp.eaten;
}
static int
sendframe(struct ct *t, Testclient *client, uchar *frame, int n, int fragment)
{
int i, m;
for(i = 0; i < n; i += m){
m = fragment && n-i > 1 ? 1 : n-i;
if(ipcsend(client->peer, frame+i, m) < 0)
return CT_ERRORF(t, "frame send failed: %s", strerror(errno));
}
return 1;
}
static void
checknoreply(struct ct *t, Testclient *client)
{
uchar byte;
ssize_t n;
errno = 0;
n = recv(client->peer, &byte, 1, MSG_PEEK|MSG_DONTWAIT);
CT_EQ_INT(t, -1, n);
CT_CHECK(t, errno == EAGAIN || errno == EWOULDBLOCK);
}
static void
waitclient(struct ct *t, Testclient *client)
{
struct stat st;
uchar token;
chanrecv(client->done, &token);
errno = 0;
if(fstat(client->fd, &st) == 0)
CT_CHECK(t, st.st_dev != client->dev || st.st_ino != client->ino);
else
CT_EQ_INT(t, EBADF, errno);
chanfree(client->done);
client->done = nil;
}
static void
disconnectclient(struct ct *t, Pump *p, Testclient *client, void *owner)
{
Keyreq req;
uchar token;
close(client->peer);
client->peer = -1;
req = nextrequest(t, p, Keyrelease);
if(owner != nil)
CT_EQ_PTR(t, owner, req.owner);
CT_CHECK(t, channbrecv(client->done, &token) <= 0);
allowrequest(p);
waitclient(t, client);
}
void
server_connection_ownership(struct ct *t)
{
Serverfix f;
Testclient a, b, c;
Keyreq req;
Str shown;
char preedit[Ipcfieldmax+1];
void *aowner, *bowner, *cowner;
uchar byte, token;
ssize_t n;
memset(&a, 0, sizeof a);
memset(&b, 0, sizeof b);
memset(&c, 0, sizeof c);
a.fd = a.peer = b.fd = b.peer = c.fd = c.peer = -1;
aowner = bowner = cowner = nil;
serverbegin(&f, 3);
if(!startclient(t, &a) || !startclient(t, &b))
goto cleanup;
if(!sendkey(t, &a, 1, 0, 'k'))
goto cleanup;
req = nextrequest(t, &f.pump, Keypress);
aowner = req.owner;
allowrequest(&f.pump);
CT_CHECK(t, readreply(t, &a, 1, "", preedit));
CT_EQ_STR(t, "k", preedit);
if(!sendkey(t, &a, 1, 0, 'a'))
goto cleanup;
req = nextrequest(t, &f.pump, Keypress);
CT_EQ_PTR(t, aowner, req.owner);
allowrequest(&f.pump);
CT_CHECK(t, readreply(t, &a, 1, "", preedit));
CT_EQ_STR(t, "", preedit);
CT_EQ_PTR(t, aowner, testengineowner());
if(!sendkey(t, &b, 1, 0, 'n'))
goto cleanup;
req = nextrequest(t, &f.pump, Keypress);
bowner = req.owner;
CT_CHECK(t, bowner != aowner);
allowrequest(&f.pump);
CT_CHECK(t, readreply(t, &b, 1, "", preedit));
CT_EQ_STR(t, "", preedit);
CT_EQ_PTR(t, bowner, testengineowner());
if(!sendreset(t, &a, 1))
goto cleanup;
req = nextrequest(t, &f.pump, Keyreset);
CT_EQ_PTR(t, aowner, req.owner);
allowrequest(&f.pump);
/* The reset hands back what the takeover took from this client. */
CT_CHECK(t, readreply(t, &a, 1, "", preedit));
CT_EQ_STR(t, "", preedit);
CT_EQ_PTR(t, bowner, testengineowner());
if(!sendkey(t, &b, 1, 0, 0))
goto cleanup;
req = nextrequest(t, &f.pump, Keypress);
allowrequest(&f.pump);
CT_CHECK(t, !readreply(t, &b, 1, "", preedit));
CT_EQ_STR(t, "", preedit);
disconnectclient(t, &f.pump, &a, aowner);
CT_EQ_PTR(t, bowner, testengineowner());
if(!sendkey(t, &b, 1, 0, 'y'))
goto cleanup;
req = nextrequest(t, &f.pump, Keypress);
allowrequest(&f.pump);
CT_CHECK(t, readreply(t, &b, 1, "", preedit));
if(!sendkey(t, &b, 1, 0, 'a'))
goto cleanup;
req = nextrequest(t, &f.pump, Keypress);
allowrequest(&f.pump);
CT_CHECK(t, readreply(t, &b, 1, "", preedit));
CT_EQ_STR(t, "にゃ", preedit);
if(!sendreset(t, &b, 1))
goto cleanup;
req = nextrequest(t, &f.pump, Keyreset);
CT_EQ_PTR(t, bowner, req.owner);
allowrequest(&f.pump);
CT_CHECK(t, readreply(t, &b, 1, "にゃ", preedit));
CT_EQ_STR(t, "", preedit);
CT_EQ_PTR(t, bowner, testengineowner());
if(!sendkey(t, &b, 0, 0, 'k'))
goto cleanup;
req = nextrequestcap(t, &f.pump, Keypress, 0);
CT_EQ_PTR(t, bowner, req.owner);
allowrequest(&f.pump);
CT_CHECK(t, readreply(t, &b, 0, "", preedit));
errno = 0;
n = recv(b.peer, &byte, 1, MSG_PEEK|MSG_DONTWAIT);
CT_EQ_INT(t, -1, n);
CT_CHECK(t, errno == EAGAIN || errno == EWOULDBLOCK);
CT_EQ_PTR(t, bowner, testengineowner());
if(!sendkey(t, &b, 1, 0, 'a'))
goto cleanup;
req = nextrequest(t, &f.pump, Keypress);
allowrequest(&f.pump);
CT_CHECK(t, readreply(t, &b, 1, "", preedit));
CT_EQ_STR(t, "", preedit);
disconnectclient(t, &f.pump, &b, bowner);
CT_EQ_PTR(t, nil, testengineowner());
testenginepreedit(&shown);
CT_EQ_INT(t, 0, shown.n);
if(!startclient(t, &c))
goto cleanup;
if(!sendkey(t, &c, 1, 0, 'k'))
goto cleanup;
req = nextrequest(t, &f.pump, Keypress);
cowner = req.owner;
close(c.peer);
c.peer = -1;
allowrequest(&f.pump);
req = nextrequest(t, &f.pump, Keyrelease);
CT_EQ_PTR(t, cowner, req.owner);
CT_EQ_PTR(t, cowner, testengineowner());
CT_CHECK(t, channbrecv(c.done, &token) <= 0);
allowrequest(&f.pump);
waitclient(t, &c);
CT_EQ_PTR(t, nil, testengineowner());
CT_CHECK(t, channbrecv(clientc, &token) <= 0);
cleanup:
if(a.peer >= 0)
disconnectclient(t, &f.pump, &a, aowner);
if(b.peer >= 0)
disconnectclient(t, &f.pump, &b, bowner);
if(c.peer >= 0)
disconnectclient(t, &f.pump, &c, cowner);
serverend(&f);
}
void
server_extension_stream(struct ct *t)
{
Serverfix f;
Testclient a, b;
Keyreq req;
uchar frame[Ipccaretsz];
char preedit[Ipcfieldmax+1];
void *aowner, *bowner;
memset(&a, 0, sizeof a);
memset(&b, 0, sizeof b);
a.fd = a.peer = b.fd = b.peer = -1;
aowner = bowner = nil;
serverbegin(&f, 2);
if(!startclient(t, &a))
goto cleanup;
/* Capability and caret frames may be split at any byte boundary. */
ipcpackcap(frame, 1);
if(!sendframe(t, &a, frame, Ipcreqsz, 1))
goto cleanup;
req = nextrequest(t, &f.pump, Keycap);
aowner = req.owner;
CT_EQ_PTR(t, nil, testengineowner());
allowrequest(&f.pump);
CT_EQ_INT(t, 1, readreply(t, &a, 1, "", preedit));
CT_EQ_STR(t, "", preedit);
ipcpackcaret(frame, 1, -101, -7, 23);
if(!sendframe(t, &a, frame, Ipccaretsz, 1))
goto cleanup;
req = nextrequest(t, &f.pump, Keycaret);
CT_EQ_PTR(t, aowner, req.owner);
CT_EQ_INT(t, 1, req.caret.valid);
CT_EQ_INT(t, -101, req.caret.x);
CT_EQ_INT(t, -7, req.caret.y);
CT_EQ_INT(t, 23, req.caret.h);
CT_EQ_PTR(t, nil, testengineowner());
allowrequest(&f.pump);
checknoreply(t, &a);
ipcpackreq(frame, 1, 0, 'k');
if(!sendframe(t, &a, frame, Ipcreqsz, 1))
goto cleanup;
req = nextrequest(t, &f.pump, Keypress);
CT_EQ_PTR(t, aowner, req.owner);
CT_EQ_INT(t, 1, req.caret.valid);
CT_EQ_INT(t, -101, req.caret.x);
CT_EQ_INT(t, -7, req.caret.y);
CT_EQ_INT(t, 23, req.caret.h);
allowrequest(&f.pump);
CT_EQ_INT(t, 1, readreply(t, &a, 1, "", preedit));
CT_EQ_STR(t, "k", preedit);
CT_EQ_PTR(t, aowner, testengineowner());
ipcpackcap(frame, 0);
if(!sendframe(t, &a, frame, Ipcreqsz, 0))
goto cleanup;
req = nextrequestcap(t, &f.pump, Keycap, 0);
CT_EQ_PTR(t, aowner, req.owner);
CT_EQ_INT(t, 1, req.caret.valid);
allowrequest(&f.pump);
CT_EQ_INT(t, 1, readreply(t, &a, 0, "", preedit));
/* Reset is still the old six-byte frame and does not discard caret. */
ipcpackreset(frame, 0);
if(!sendframe(t, &a, frame, Ipcreqsz, 1))
goto cleanup;
req = nextrequestcap(t, &f.pump, Keyreset, 0);
CT_EQ_PTR(t, aowner, req.owner);
CT_EQ_INT(t, 1, req.caret.valid);
CT_EQ_INT(t, -101, req.caret.x);
CT_EQ_INT(t, -7, req.caret.y);
CT_EQ_INT(t, 23, req.caret.h);
allowrequest(&f.pump);
CT_EQ_INT(t, 1, readreply(t, &a, 0, "k", preedit));
ipcpackcap(frame, 1);
if(!sendframe(t, &a, frame, Ipcreqsz, 0))
goto cleanup;
req = nextrequest(t, &f.pump, Keycap);
allowrequest(&f.pump);
CT_EQ_INT(t, 1, readreply(t, &a, 1, "", preedit));
CT_EQ_STR(t, "", preedit);
ipcpackreq(frame, 1, 0, 'n');
if(!sendframe(t, &a, frame, Ipcreqsz, 0))
goto cleanup;
req = nextrequest(t, &f.pump, Keypress);
CT_EQ_INT(t, 1, req.caret.valid);
CT_EQ_INT(t, -101, req.caret.x);
allowrequest(&f.pump);
CT_EQ_INT(t, 1, readreply(t, &a, 1, "", preedit));
CT_EQ_STR(t, "", preedit);
disconnectclient(t, &f.pump, &a, aowner);
/* Disconnect drops state; a legacy client gets the historical defaults. */
if(!startclient(t, &b) || !sendkey(t, &b, 1, 0, 'k'))
goto cleanup;
req = nextrequest(t, &f.pump, Keypress);
bowner = req.owner;
CT_EQ_INT(t, 0, req.caret.valid);
allowrequest(&f.pump);
CT_EQ_INT(t, 1, readreply(t, &b, 1, "", preedit));
CT_EQ_STR(t, "k", preedit);
disconnectclient(t, &f.pump, &b, bowner);
cleanup:
if(a.peer >= 0)
disconnectclient(t, &f.pump, &a, aowner);
if(b.peer >= 0)
disconnectclient(t, &f.pump, &b, bowner);
serverend(&f);
}
void
server_rejects_unknown_extension(struct ct *t)
{
Serverfix f;
Testclient badver, badop, good;
Keyreq req;
uchar frame[Ipccaretsz];
char preedit[Ipcfieldmax+1];
void *owner;
memset(&badver, 0, sizeof badver);
memset(&badop, 0, sizeof badop);
memset(&good, 0, sizeof good);
badver.fd = badver.peer = badop.fd = badop.peer = -1;
good.fd = good.peer = -1;
owner = nil;
serverbegin(&f, 3);
if(!startclient(t, &badver))
goto cleanup;
ipcpackcaret(frame, 1, 3, 4, 5);
frame[1]++;
if(!sendframe(t, &badver, frame, sizeof frame, 0))
goto cleanup;
req = nextrequest(t, &f.pump, Keyrelease);
allowrequest(&f.pump);
waitclient(t, &badver);
close(badver.peer);
badver.peer = -1;
if(!startclient(t, &badop))
goto cleanup;
ipcpackcaret(frame, 1, 3, 4, 5);
frame[2]++;
if(!sendframe(t, &badop, frame, sizeof frame, 1))
goto cleanup;
req = nextrequest(t, &f.pump, Keyrelease);
allowrequest(&f.pump);
waitclient(t, &badop);
close(badop.peer);
badop.peer = -1;
/* A malformed peer must not disturb another client connection. */
if(!startclient(t, &good) || !sendkey(t, &good, 1, 0, 'k'))
goto cleanup;
req = nextrequest(t, &f.pump, Keypress);
owner = req.owner;
allowrequest(&f.pump);
CT_EQ_INT(t, 1, readreply(t, &good, 1, "", preedit));
CT_EQ_STR(t, "k", preedit);
disconnectclient(t, &f.pump, &good, owner);
cleanup:
if(badver.peer >= 0)
disconnectclient(t, &f.pump, &badver, nil);
if(badop.peer >= 0)
disconnectclient(t, &f.pump, &badop, nil);
if(good.peer >= 0)
disconnectclient(t, &f.pump, &good, owner);
serverend(&f);
}

View File

@@ -1,64 +0,0 @@
#!/usr/bin/env python3
import subprocess
import tempfile
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
CONVERTER = ROOT / "map" / "skk2ktrans"
FIXTURES = Path(__file__).parent / "data" / "skk"
TIMEOUT = 10
class Skk2KtransTest(unittest.TestCase):
def test_euc_jp_conversion_is_stable_and_literal(self):
source = (FIXTURES / "source.skk.utf8").read_text(encoding="utf-8")
expected = (FIXTURES / "expected.tsv").read_bytes()
with tempfile.TemporaryDirectory() as directory:
input_path = Path(directory) / "source.skk"
input_path.write_bytes(source.encode("euc_jp"))
result = subprocess.run(
[CONVERTER, input_path], capture_output=True, check=False,
timeout=TIMEOUT)
self.assertEqual(result.returncode, 0, result.stderr.decode())
self.assertEqual(result.stdout, expected)
def test_invalid_euc_jp_is_rejected(self):
result = subprocess.run(
[CONVERTER], input=b"\xff\xff\n", capture_output=True, check=False,
timeout=TIMEOUT)
self.assertNotEqual(result.returncode, 0)
self.assertTrue(result.stderr)
def test_escaped_candidate_is_rejected(self):
source = b"key /escaped\\/slash/\n"
result = subprocess.run(
[CONVERTER], input=source, capture_output=True, check=False,
timeout=TIMEOUT)
self.assertNotEqual(result.returncode, 0)
self.assertIn(b"escaped candidates are unsupported", result.stderr)
def test_expression_candidate_is_rejected(self):
source = b'key /(concat "a/b")/literal/\n'
result = subprocess.run(
[CONVERTER], input=source, capture_output=True, check=False,
timeout=TIMEOUT)
self.assertNotEqual(result.returncode, 0)
self.assertIn(b"expression candidates are unsupported", result.stderr)
def test_files_have_distinct_row_boundaries(self):
with tempfile.TemporaryDirectory() as directory:
first = Path(directory) / "first.skk"
second = Path(directory) / "second.skk"
first.write_bytes(b"a /A/")
second.write_bytes(b"b /B/\n")
result = subprocess.run(
[CONVERTER, first, second], capture_output=True, check=False,
timeout=TIMEOUT)
self.assertEqual(result.returncode, 0, result.stderr.decode())
self.assertEqual(result.stdout, b"a\tA\nb\tB\n")
if __name__ == "__main__":
unittest.main()

View File

@@ -1,124 +0,0 @@
#include "dat.h"
#include "fn.h"
#include "test.h"
void
str_init_utf8(struct ct *t)
{
static char badtail[] = { 'a', (char)0x80 };
static char incomplete[] = { (char)0xea, (char)0xb0 };
static char overlong[] = { (char)0xc0, (char)0xaf };
static char outofrange[] = {
(char)0xf4, (char)0x90, (char)0x80, (char)0x80,
};
static char surrogate[] = {
(char)0xed, (char)0xa0, (char)0x80,
};
static char unexpected[] = { (char)0x80 };
static const struct {
char *name;
char *src;
int n;
int ok;
char *want;
} cases[] = {
{ "canonical Runeerror", "\xef\xbf\xbd", 3, 1, "\xef\xbf\xbd" },
{ "incomplete", incomplete, sizeof incomplete, 0, "" },
{ "unexpected continuation", unexpected, sizeof unexpected, 0, "" },
{ "overlong", overlong, sizeof overlong, 0, "" },
{ "surrogate", surrogate, sizeof surrogate, 0, "" },
{ "out of range", outofrange, sizeof outofrange, 0, "" },
{ "bad tail", badtail, sizeof badtail, 0, "" },
};
char full[Maxrunes+2];
Str s;
int i;
s = mkstr("A한😀");
CT_EQ_INT(t, 3, s.n);
checkstr(t, "round trip", "A한😀", &s);
for(i = 0; i < nelem(cases); i++){
memset(&s, 0xa5, sizeof s);
if(sinit(&s, cases[i].src, cases[i].n) != cases[i].ok)
CT_ERRORF(t, "%s: wrong validity", cases[i].name);
checkstr(t, cases[i].name, cases[i].want, &s);
}
memset(full, 'a', sizeof full);
full[sizeof full-1] = '\0';
CT_CHECK(t, !sinit(&s, full, strlen(full)));
CT_EQ_INT(t, 0, s.n);
}
void
str_edit_and_alias(struct ct *t)
{
Str s;
int i;
s = mkstr("ab");
sappend(&s, &s);
checkstr(t, "self append", "abab", &s);
for(i = 0; i < 40; i++)
s.r[i] = 'a' + i % 26;
s.n = 40;
sappend(&s, &s);
CT_EQ_INT(t, Maxrunes, s.n);
for(i = 0; i < 24; i++)
if(s.r[40+i] != s.r[i])
CT_ERRORF(t, "capped self append differs at rune %d", i);
}
void
str_utf8_capacity(struct ct *t)
{
static const struct {
int size;
int n;
char *want;
} cases[] = {
{ 1, 0, "" },
{ 2, 1, "a" },
{ 3, 1, "a" },
{ 4, 1, "a" },
{ 5, 4, "a한" },
{ 6, 5, "a한b" },
};
char buf[16];
Str s;
int i, n;
s = mkstr("a한b");
for(i = 0; i < nelem(cases); i++){
memset(buf, 'Z', sizeof buf);
n = stoutf(&s, buf, cases[i].size);
if(n != cases[i].n)
CT_ERRORF(t, "size %d: want length %d, got %d",
cases[i].size, cases[i].n, n);
if(strcmp(cases[i].want, buf) != 0)
CT_ERRORF(t, "size %d: want \"%s\", got \"%s\"",
cases[i].size, cases[i].want, buf);
CT_EQ_INT(t, 'Z', buf[cases[i].size]);
}
s = mkstr("😀");
memset(buf, 'Z', sizeof buf);
CT_EQ_INT(t, 0, stoutf(&s, buf, 4));
CT_EQ_STR(t, "", buf);
CT_EQ_INT(t, 4, stoutf(&s, buf, 5));
CT_EQ_STR(t, "😀", buf);
CT_EQ_INT(t, 'Z', buf[5]);
}
void
str_invalid_and_full_appends(struct ct *t)
{
Str s;
int i;
for(i = 0; i < Maxrunes; i++)
s.r[i] = 'a';
s.n = Maxrunes;
sputr(&s, 'z');
CT_EQ_INT(t, Maxrunes, s.n);
CT_EQ_INT(t, 'a', s.r[Maxrunes-1]);
}

View File

@@ -1,133 +0,0 @@
/* Included after dat.h and fn.h (or after the source under test). */
#include "cutest/cutest.h"
/*
* A test's stand-in for imthread: handles engine requests from keyc,
* tracing each one first. Requests whose op is holdop (every request
* when holdop is Pumpall) wait on go before the engine sees them.
*/
typedef struct Pump Pump;
struct Pump
{
Channel *trace;
Channel *go;
Channel *stop;
Channel *done;
int holdop;
int active;
};
enum
{
Pumpnone = -1,
Pumpall = -2,
};
Str mkstr(char*);
int checkstr(struct ct*, char*, char*, Str*);
void checkenginepreedit(struct ct*, char*);
void pumpstart(Pump*, int);
void pumphold(Pump*, int);
void pumpstop(Pump*);
/* Engine internals reached through engine_test.c. */
void testengineinit(int);
void testenginehandle(Keyreq*);
void* testengineowner(void);
void testenginepreedit(Str*);
void testenginecaret(Caret*);
void str_init_utf8(struct ct*);
void str_edit_and_alias(struct ct*);
void str_utf8_capacity(struct ct*);
void str_invalid_and_full_appends(struct ct*);
void trie_exact_prefix_and_duplicate(struct ct*);
void trie_put_and_unloaded(struct ct*);
void popup_layout(struct ct*);
void font_render(struct ct*);
void production_maps_load(struct ct*);
void transmap_states(struct ct*);
void korean_sequences(struct ct*);
void korean_compound_vowels(struct ct*);
void korean_compound_finals(struct ct*);
void korean_backspace(struct ct*);
void vietnamese_transitions(struct ct*);
void vietnamese_backspace(struct ct*);
void vietnamese_state_lifetime(struct ct*);
void engine_clears_candidates(struct ct*);
void engine_backspace_clears_candidates(struct ct*);
void engine_selects_visible_candidate(struct ct*);
void engine_candidate_shortcut_modifiers(struct ct*);
void engine_candidate_page_metadata(struct ct*);
void engine_candidate_page_snapshots(struct ct*);
void engine_candidate_page_movement(struct ct*);
void engine_candidate_completion(struct ct*);
void engine_active_owner_lifecycle(struct ct*);
void engine_active_owner_reset(struct ct*);
void engine_active_owner_caret(struct ct*);
void engine_popup_preedit_capability(struct ct*);
void engine_vietnamese_client_preedit(struct ct*);
void engine_commit_contract(struct ct*);
void engine_language_switch_state(struct ct*);
void engine_telex_history_bound(struct ct*);
void engine_korean_modifiers_and_backspace(struct ct*);
void engine_direct_language_modes(struct ct*);
void engine_japanese_readings(struct ct*);
void engine_japanese_candidates(struct ct*);
void engine_japanese_backspace_and_boundaries(struct ct*);
void engine_katakana_sequences(struct ct*);
void engine_emoji_single_candidate(struct ct*);
void engine_emoji_queries(struct ct*);
void engine_emoji_japanese_and_multirune(struct ct*);
void engine_emoji_digit_aliases(struct ct*);
void engine_emoji_navigation(struct ct*);
void engine_search_candidate_keys(struct ct*);
void engine_emoji_preedit_languages(struct ct*);
void engine_emoji_start_and_unknown(struct ct*);
void engine_hanja_search(struct ct*);
void engine_hanja_unknown_and_cancel(struct ct*);
void engine_hanja_reaches_back(struct ct*);
void engine_hanja_word_prefix(struct ct*);
void engine_hanja_korean_keys(struct ct*);
void engine_hanja_backspace(struct ct*);
void engine_hanja_input_languages(struct ct*);
void engine_randomized_stress(struct ct*);
void engine_full_boundary_passthrough(struct ct*);
void dictionary_candidates(struct ct*);
void dictionary_misses(struct ct*);
void dictionary_prefix(struct ct*);
void ipc_masks_modifiers(struct ct*);
void ipc_control_and_caret_frames(struct ct*);
void ipc_runtime_path(struct ct*);
void ipc_response_pack_boundaries(struct ct*);
void ipc_response_empty_and_preedit(struct ct*);
void ipc_response_max_and_drain(struct ct*);
void ipc_response_fragmented_and_truncated(struct ct*);
void ipc_broken_peer_send(struct ct*);
void server_connection_ownership(struct ct*);
void server_extension_stream(struct ct*);
void server_rejects_unknown_extension(struct ct*);
void ibus_machine_id_fallback(struct ct*);
void ibus_capability_policy(struct ct*);
void ibus_private_input_policy(struct ct*);
void ibus_context_lifecycle(struct ct*);
void ibus_active_release_lifecycle(struct ct*);
void compose_sequences(struct ct*);
void xim_keymap_lookup(struct ct*);
void xim_compound_text(struct ct*);
void xim_adapter_key_contract(struct ct*);
void xim_adapter_release_lifecycle(struct ct*);
void xim_adapter_free_waits_for_release(struct ct*);
void xim_adapter_styles(struct ct*);
void xim_adapter_placement(struct ct*);
void xim_adapter_placement_updates(struct ct*);
void xim_adapter_callback_replacement(struct ct*);
void xim_adapter_callback_unicode(struct ct*);
void xim_adapter_callback_cleanup(struct ct*);
void xim_adapter_callback_transfer(struct ct*);
void xim_adapter_callback_owner_loss(struct ct*);
void xim_adapter_commit_encoding(struct ct*);
void wl_modifier_mask(struct ct*);
void wl_forwarded_keys(struct ct*);
void wl_surrounding_text(struct ct*);
void wl_repeat_ends(struct ct*);

View File

@@ -1,104 +0,0 @@
#include "dat.h"
#include "fn.h"
#include "test.h"
Str
mkstr(char *s)
{
Str r;
sinit(&r, s, strlen(s));
return r;
}
int
checkstr(struct ct *t, char *where, char *want, Str *got)
{
char buf[Maxutf];
stoutf(got, buf, sizeof buf);
if(strcmp(want, buf) == 0)
return 1;
return CT_ERRORF(t, "%s: want \"%s\", got \"%s\"",
where, want, buf);
}
void
checkenginepreedit(struct ct *t, char *want)
{
Str preedit;
testenginepreedit(&preedit);
checkstr(t, "engine preedit", want, &preedit);
}
static void
pumpthread(void *arg)
{
Pump *p;
Keyreq req;
uchar token;
Alt alts[] = {
{nil, &req, CHANRCV, nil},
{nil, &token, CHANRCV, nil},
{nil, nil, CHANEND, nil},
};
p = arg;
alts[0].c = keyc;
alts[1].c = p->stop;
for(;;)
switch(alt(alts)){
case 0:
chansend(p->trace, &req);
if(p->holdop == Pumpall || p->holdop == req.op)
chanrecv(p->go, &token);
testenginehandle(&req);
break;
case 1:
chansend(p->done, &token);
return;
}
}
/* ntrace is the trace channel's depth: 0 makes every trace a rendezvous. */
void
pumpstart(Pump *p, int ntrace)
{
memset(p, 0, sizeof *p);
p->trace = chancreate(sizeof(Keyreq), ntrace);
p->go = chancreate(sizeof(uchar), 0);
p->stop = chancreate(sizeof(uchar), 0);
p->done = chancreate(sizeof(uchar), 0);
p->holdop = Pumpnone;
/* Its own proc: tests block in socket I/O while the engine runs. */
proccreate(pumpthread, p, 8192);
p->active = 1;
}
void
pumphold(Pump *p, int op)
{
p->holdop = op;
}
/* Every held request must have been let go before stopping. */
void
pumpstop(Pump *p)
{
Keyreq req;
uchar token;
if(!p->active)
return;
while(channbrecv(p->trace, &req) > 0)
;
token = 0;
chansend(p->stop, &token);
chanrecv(p->done, &token);
chanfree(p->trace);
chanfree(p->go);
chanfree(p->stop);
chanfree(p->done);
p->active = 0;
}

View File

@@ -1,132 +0,0 @@
#include "dat.h"
#include "fn.h"
#include "test.h"
void
trie_exact_prefix_and_duplicate(struct ct *t)
{
static const struct {
char *key;
int match;
char *want;
} cases[] = {
{ "a", TrieExact, "alpha" },
{ "ab", TrieExact, "beta" },
{ "", TrieExact, "" },
{ "dupli", TriePrefix, nil },
{ "duplicate", TrieExact, "second" },
{ "missing", TrieMiss, nil },
{ "", TriePrefix, nil },
};
char *v;
Trie *trie;
Str key;
int i, match, n;
trie = trieopen("data/trie.map");
for(i = 0; i < nelem(cases); i++){
key = mkstr(cases[i].key);
match = trielookup(trie, &key, &v, &n);
if(match != cases[i].match){
CT_ERRORF(t, "case %d: want match %d, got %d",
i, cases[i].match, match);
continue;
}
if(cases[i].want == nil){
CT_EQ_PTR(t, nil, v);
CT_EQ_INT(t, 0, n);
}else{
CT_EQ_INT(t, strlen(cases[i].want), n);
CT_EQ_MEM(t, cases[i].want, v, n);
}
}
trieclose(trie);
}
void
trie_put_and_unloaded(struct ct *t)
{
char *v;
Trie *trie;
Str key;
int n;
key = mkstr("k");
CT_EQ_INT(t, TrieMiss, trielookup(nil, &key, &v, &n));
trie = trienew();
CT_EQ_INT(t, TrieMiss, trielookup(trie, &key, &v, &n));
trieput(trie, "k", 1, "one two", 7);
trieput(trie, "ka", 2, "", 0);
if(CT_EQ_INT(t, TrieExact, trielookup(trie, &key, &v, &n)))
CT_EQ_MEM(t, "one two", v, 7);
key = mkstr("ka");
if(CT_EQ_INT(t, TrieExact, trielookup(trie, &key, &v, &n)))
CT_EQ_INT(t, 0, n);
trieput(trie, "k", 1, "three", 5);
key = mkstr("k");
if(CT_EQ_INT(t, TrieExact, trielookup(trie, &key, &v, &n)))
CT_EQ_MEM(t, "three", v, 5);
trieclose(trie);
}
void
production_maps_load(struct ct *t)
{
char path[256];
Trie *map;
int i, loaded, registered;
loaded = registered = 0;
for(i = 0; i < nlang; i++){
if(langs[i].mapname == nil)
continue;
registered++;
snprint(path, sizeof path, "../map/%s.map", langs[i].mapname);
map = trieopen(path);
if(!CT_CHECK(t, map != nil))
continue;
trieclose(map);
loaded++;
}
CT_CHECK(t, registered > 0);
CT_EQ_INT(t, registered, loaded);
}
void
transmap_states(struct ct *t)
{
static const struct {
char *pre;
Rune key;
int eat;
char *emit;
char *next;
} cases[] = {
{ "", 'k', 1, "", "k" },
{ "k", 'a', 1, "", "ka" },
{ "ka", 's', 1, "", "s" },
{ "ka", 'q', 0, "", "" },
{ "k", 'q', 0, "k", "" },
};
Emit e;
Im state;
Trie *fixture, *saved;
int i;
fixture = trieopen("data/hira.map");
memset(&state, 0, sizeof state);
state.l = getlang(LangJP);
saved = state.l->map;
state.l->map = fixture;
for(i = 0; i < nelem(cases); i++){
state.pre = mkstr(cases[i].pre);
e = transmap(&state, cases[i].key);
if(e.eat != cases[i].eat)
CT_ERRORF(t, "case %d: want eat %d, got %d",
i, cases[i].eat, e.eat);
checkstr(t, "emit", cases[i].emit, &e.s);
checkstr(t, "next", cases[i].next, &e.next);
}
trieclose(fixture);
state.l->map = saved;
}

View File

@@ -1,177 +0,0 @@
#define CT_IMPLEMENTATION
#include "dat.h"
#include "fn.h"
#include "test.h"
#include <stdarg.h>
Channel *drawc;
Channel *keyc;
void
die(char *fmt, ...)
{
va_list ap;
fprint(2, "unit_test: ");
va_start(ap, fmt);
vfprint(2, fmt, ap);
va_end(ap);
fprint(2, "\n");
threadexitsall("die");
}
void*
emalloc(ulong n)
{
void *p;
p = mallocz(n, 1);
if(p == nil)
die("out of memory");
return p;
}
void*
erealloc(void *p, ulong n)
{
p = realloc(p, n);
if(p == nil)
die("out of memory");
return p;
}
static void
testmapinit(void)
{
Lang *jp, *kata, *vi;
jp = getlang(LangJP);
kata = getlang(LangJPK);
vi = getlang(LangVI);
if(jp == nil || kata == nil || vi == nil)
die("test language is not registered");
jp->map = trieopen("../map/hira.map");
kata->map = trieopen("../map/kata.map");
vi->map = trieopen("../map/telex.map");
}
#ifndef STRESS
static const struct ct_test tests[] = {
{ "str/init-utf8", str_init_utf8 },
{ "str/edit-and-alias", str_edit_and_alias },
{ "str/utf8-capacity", str_utf8_capacity },
{ "str/invalid-full-appends", str_invalid_and_full_appends },
{ "trie/exact-prefix-duplicate", trie_exact_prefix_and_duplicate },
{ "trie/put-and-unloaded", trie_put_and_unloaded },
{ "popup/layout", popup_layout },
{ "font/render", font_render },
{ "map/production-lifecycle", production_maps_load },
{ "transmap/states", transmap_states },
{ "hangul/sequences", korean_sequences },
{ "hangul/compound-vowels", korean_compound_vowels },
{ "hangul/compound-finals", korean_compound_finals },
{ "hangul/backspace", korean_backspace },
{ "telex/transitions", vietnamese_transitions },
{ "telex/backspace", vietnamese_backspace },
{ "telex/state-lifetime", vietnamese_state_lifetime },
{ "engine/clears-candidates", engine_clears_candidates },
{ "engine/backspace-clears-candidates", engine_backspace_clears_candidates },
{ "engine/selects-visible-candidate", engine_selects_visible_candidate },
{ "engine/candidate-shortcut-modifiers", engine_candidate_shortcut_modifiers },
{ "engine/candidate-page-metadata", engine_candidate_page_metadata },
{ "engine/candidate-page-snapshots", engine_candidate_page_snapshots },
{ "engine/candidate-page-movement", engine_candidate_page_movement },
{ "engine/candidate-completion", engine_candidate_completion },
{ "engine/active-owner-lifecycle", engine_active_owner_lifecycle },
{ "engine/active-owner-reset", engine_active_owner_reset },
{ "engine/active-owner-caret", engine_active_owner_caret },
{ "engine/popup-preedit-capability", engine_popup_preedit_capability },
{ "engine/vietnamese-client-preedit", engine_vietnamese_client_preedit },
{ "engine/commit-contract", engine_commit_contract },
{ "engine/language-switch-state", engine_language_switch_state },
{ "engine/telex-history-bound", engine_telex_history_bound },
{ "engine/korean-modifiers-backspace", engine_korean_modifiers_and_backspace },
{ "engine/direct-language-modes", engine_direct_language_modes },
{ "engine/japanese-readings", engine_japanese_readings },
{ "engine/japanese-candidates", engine_japanese_candidates },
{ "engine/japanese-backspace-boundaries", engine_japanese_backspace_and_boundaries },
{ "engine/katakana-sequences", engine_katakana_sequences },
{ "engine/emoji-single-candidate", engine_emoji_single_candidate },
{ "engine/emoji-queries", engine_emoji_queries },
{ "engine/emoji-japanese-multirune", engine_emoji_japanese_and_multirune },
{ "engine/emoji-digit-aliases", engine_emoji_digit_aliases },
{ "engine/emoji-navigation", engine_emoji_navigation },
{ "engine/search-candidate-keys", engine_search_candidate_keys },
{ "engine/emoji-preedit-languages", engine_emoji_preedit_languages },
{ "engine/emoji-start-and-unknown", engine_emoji_start_and_unknown },
{ "engine/hanja-search", engine_hanja_search },
{ "engine/hanja-unknown-cancel", engine_hanja_unknown_and_cancel },
{ "engine/hanja-reaches-back", engine_hanja_reaches_back },
{ "engine/hanja-word-prefix", engine_hanja_word_prefix },
{ "engine/hanja-korean-keys", engine_hanja_korean_keys },
{ "engine/hanja-backspace", engine_hanja_backspace },
{ "engine/hanja-input-languages", engine_hanja_input_languages },
{ "engine/full-boundary-passthrough", engine_full_boundary_passthrough },
{ "dict/candidates", dictionary_candidates },
{ "dict/misses", dictionary_misses },
{ "dict/prefix", dictionary_prefix },
{ "ipc/masks-modifiers", ipc_masks_modifiers },
{ "ipc/control-caret-frames", ipc_control_and_caret_frames },
{ "ipc/runtime-path", ipc_runtime_path },
{ "ipc/response-pack-boundaries", ipc_response_pack_boundaries },
{ "ipc/response-empty-preedit", ipc_response_empty_and_preedit },
{ "ipc/response-max-drain", ipc_response_max_and_drain },
{ "ipc/response-fragmented-truncated", ipc_response_fragmented_and_truncated },
{ "ipc/broken-peer-send", ipc_broken_peer_send },
{ "server/connection-ownership", server_connection_ownership },
{ "server/extension-stream", server_extension_stream },
{ "server/rejects-unknown-extension", server_rejects_unknown_extension },
{ "ibus/machine-id-fallback", ibus_machine_id_fallback },
{ "ibus/capability-policy", ibus_capability_policy },
{ "ibus/private-input-policy", ibus_private_input_policy },
{ "ibus/context-lifecycle", ibus_context_lifecycle },
{ "ibus/active-release-lifecycle", ibus_active_release_lifecycle },
{ "compose/sequences", compose_sequences },
{ "xim/keymap-lookup", xim_keymap_lookup },
{ "xim/compound-text", xim_compound_text },
{ "xim/adapter-key-contract", xim_adapter_key_contract },
{ "xim/adapter-release-lifecycle", xim_adapter_release_lifecycle },
{ "xim/adapter-free-waits-release", xim_adapter_free_waits_for_release },
{ "xim/styles", xim_adapter_styles },
{ "xim/placement", xim_adapter_placement },
{ "xim/placement-updates", xim_adapter_placement_updates },
{ "xim/callback-replacement", xim_adapter_callback_replacement },
{ "xim/callback-unicode", xim_adapter_callback_unicode },
{ "xim/callback-cleanup", xim_adapter_callback_cleanup },
{ "xim/callback-transfer", xim_adapter_callback_transfer },
{ "xim/callback-owner-loss", xim_adapter_callback_owner_loss },
{ "xim/commit-encoding", xim_adapter_commit_encoding },
{ "wl/modifier-mask", wl_modifier_mask },
{ "wl/forwarded-keys", wl_forwarded_keys },
{ "wl/surrounding-text", wl_surrounding_text },
{ "wl/repeat-ends", wl_repeat_ends },
};
#else
static const struct ct_test tests[] = {
{ "engine/randomized-stress", engine_randomized_stress },
};
#endif
void
threadmain(int argc, char **argv)
{
int i, status;
drawc = chancreate(sizeof(Drawcmd), 4);
keyc = chancreate(sizeof(Keyreq), 0);
testmapinit();
status = CT_RUN_ARGS(tests, argc, argv);
for(i = 0; i < nlang; i++){
trieclose(langs[i].map);
langs[i].map = nil;
}
chanfree(drawc);
chanfree(keyc);
threadexitsall(status == 0 ? nil : "tests failed");
}

Some files were not shown because too many files have changed in this diff Show More