about summary refs log tree commit diff stats
path: root/CHANGELOG
blob: dbe511708b322eff31074b6e25efea71d823ce8c (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
0.8.1 (2020-02-07)
=====

This is a bugfix release.

If you like Profanity, please consider donating: https://profanity-im.github.io/donate.html

Changes:

- Fix ending ncurses colors in rosterwin
- Fix segfault when consistent nick colors where never in config (#1236)
- Improve `/clear` behaviour to also clear the buffer. So a redraw doesn't bring
  the content back.
- Use unique IDs in stanzas when requesting avatars

0.8.0 (2020-02-03)
=====

On 2012-02-02 the first commit to Profanity was done by @boothj5.
8 years later, and 5 months after 0.7.0, we are happy to announce Profanity 0.8.0.

This release has 315 commits since the last release.
11 people contributed code to it: @paulfariello, @svensp, @aaptel, @aszlig, @mdosch, @pasis, @mzagozen, @quite, @weiss, @Misaflo and @jubalh.

Changes:

- OMEMO fingerprint autocompletion now considers only the contact in question (@paulfariello) (#1068)
- Save and display oldest timestamp from delay tags (#1254)
- Change theme handling (#1077)
  `/theme load themename` not loads only the colours of a theme. So the users preferences don't get overwritten
  `/theme full-load themename` loads the whole theme including preferences (like `omemo.char`)
- Add gruvbox theme (@Misaflo) (#1259)
- XEP-0092: Include OS name in `/software` answer.
  See `/os on|off`
- Add option to notify about version request via XEP-0092 and XEP-0232 via `adv.notify.discoversion`
  setting in configuration file.
-  Add option to display MUC name or JID in titlebar
  * `/titlebar use [name|jid]`.
-  Add `/roster room use` command to decide whether to display the MUC name or JID in the roster
  * `/roster room use name` to use the name of the MUC in the roster list.
  * `/roster room use jid` to use the jid of the MUC in the roster list.
- Fix decryption failure for messages sent from Converse.js (@paulfariello) (#1253)
- Fix crash when OMEMO key is misconfigured (@paulfariello) (#1239)
- Use OMEMO for offline MUC members (@paulfariello) (#1242) 
- Fix OMEMO 1on1 chats with psi+ (@svensp) (#1247)
- Save occupants and roster chars (#1244)
- Add support for downloading user avatars via XEP-0084 (#1240)
  See `/avatar`
- Add last read position marker trackbar (#1238)
- Dont print error message if a valid setting function is called (#1237)
  When printing configuration we often use the same command as setting but without any argument.
  So far ths displayed "Invalid usage".
- Add support for XEP-0392 consistent color generation for nicks (@aaptel and @jubalh) (#1191)
  See `/color`, `/occupants color`, `/roster color`
- Call ncurses resize function before move function (#1235)
- Fix error when joining IRC via biboumi (#1230)
- Fix stanza_get_child_by_name_and_from() strcmp (#1227)
- Fix showing own messages twice in ejabberd mucs (#1223)
- Allow setting of status message (which actually never worked before) (b846c49)
  `/status set online "This is my text"`
  `/status set away bye`
  `/status set away`
- Add option to store current line in history (irssi down arrow) (#200)
- Standardize commands (#1116):
  * `/tls show on|of -> `/titlebar show|hide tls`
  * `/encwarn on|off` -> `/titlebar show|hide encwarn`
  * `/titlebar show|hide resource` additionally to `/resource titlebar on`
  * `/titlebar show|hide presence` additionally to `/presence titlebar on`
  * `/invite <contact>` -> `/invite send <contact>
  * `/invites` -> `/invite list`
  * `/decline` -> `/invite decline`
  * `/online`, `/away`, `/dnd`, `/chat`, `/xa` -> `/status set online` etc.
  * `/status` -> `/status get`
  * `/chlog on` -> `/logging chat on` (#1224)
  * `/grlog on` -> `/logging group on` (#1224)
  * `/group` -> `/roster group` (#1229)
- Check omemo stanza names when iterating nodes (@aszlig) (#1217)
- Add clipboard feature. See `/paste` (#1216)
- Log MUC PM messages (#1214)
- Improve forest theme
- Change default text colors to default instead of white (@mdosch) (#1213) (#535)
- Fix date display in chat history (#922)
- Log incoming MUC messages if origin-id sais they dont come from us (#1201)
- Support XEP-0359: Unique and Stable Stanza IDs (#1207)
  To help us identify which MUC messages were sent by us.
- Fix double _XOPEN_SOURCE definition (@pasis) (#1206)
- Highlight unread messages with a different color in /wins (#895)
  New theming option `cmd.wins.unread`
- Improve MUC 1:1 logging (#1184)
- Fix user messages double logged in MUC logs (#1201)
- Fix 26 causes for memory leaks
- Add `/clear` autocompletion and improve help (#855)
- Allow multiple instances to write to account config via `/reload`
  command that reloads the config file (#627)
- Log message carbons to log file (#1181)
- Include PYTHON_EXTRA_LIBS in libtool config (@mzagozen) (#1200)
- Document how to block users in MUCS (#618)
- Keyboard switch to select the next window with unread messages (#1114)
  (alt + a)
- Keyboard switches (for moving to 20 windows instead of 10 (#1114)
  (alt + 1234567890qwertyuio)
- Fix MUC history detection with older Prosody versions (#1190)
  See https://issues.prosody.im/1416
- `statusbar.current` is a new theming option for currently selected tab (@quite) (#1195)
- Code cleanup
- Make /info more user friendly (#1194)
- Add solarized theme (#1175, #1198, #1199) (@mdosch)
- Set nonblocking mode for stderr (@pasis) (#1192)
- Log alleged roster push (6cf06dc)
- Allow colorization of history messages (#1170)
  See `main.text.history` theming option
- Fix crash if source jid doesn't contain the node part (@pasis) (#1153)
- Add support for 256 colors (@aaptel and @jubalh) (#1177 and many commits)
  We use the Xterm color names. See https://jonasjacek.github.io/colors
- Don't render (all) delayed messages as MUC history (@weiss) (#1173)
- Add possibility to specify alternative config file (10ca3e)
  Use `profanity -c ~/path/to/other/config`
- Don't print subscribed message if contact is already in roster (#1166)
- Only save preferences when new `/save` command is issued (#1146)

0.7.1 (2019-09-24)
=====

- Fix copyright/info displayed email
- Fix typos in OMEMO logs
- Fix crash when jid has no node part (#1153, #1193)

0.7.0 (2019-07-31)
=====

- Fix plugin unload return code if just one plugin fails (#995)
- Fix several typos
- Fix some display indentation issues (#1073)
- Fail plugin unload if the plugin doesnt exist
- Improve encrypted message stub header by mentioning the encryption method
- Fix GPG encryption (#997)
- Redraw sceen after entry of PGP key (#906)
- Fix support for case-sensitive account names (#725)
- Fix /me display when highlighting user in MUCs (#950)
- Fix `make dist` (0f0659a)
- Fix use after free bug (#1044)
- Fix segfault on connect with default account (#1046)
- Implement OMEMO support (#1039, #658, #1070)
- Add random string at the end of the default resource (#1053)
- Fix handling of messages without ID in MUC (#1061)
- Add library versioning to libprofanity (#973)
- Add more customization: occupants indent (/occupants) (#690, #1072)
- Add more customization: occupants header char (/occupants) (#690, #1074)
- Add more customization: occupants wrap (/occupants) (#690, 125ca2f)
- Add more customization: occupants char (/occupants) (#690, #1084)
- Fix formatting for privileges on (a666f0d)
- Fix usage of statusbar number in theme (#1078)
- Fix Debian 32bit tests (#1091)
- Fix unit tests (#1092)
- Fix infinite loop on connection loss (#1103)
- Don't clear saved account data in session_disconnect (#1106)
- Cancel autoping timer on disconnect or connection loss (#1105)
- Fix SIGABRT on connection loss (#1083)
- Only print room history for new messages upon reconnect (#704, #1110)
- Check if valid account before setting autoconnect (#1112)
- Improve plugin load error message, in case built without support (cc697de)
- Iterate up to 100 logfiles (#519)
- Fix rejoining of MUCs upon reconnect (#1120)
- Add option to set all window related time formats (#632, #1120)
- Always check for directory changes with sendfile auto completion (#1154)
- Fix several memory leaks (#1130, + plenty commits)
- For details see https://github.com/profanity-im/profanity/milestone/17

0.6.0 (2019-02-18)
=====

- Allow moving vertical window positions (/titlebar, /mainwin, /statusbar, /inputwin)
- Allow loading/unloading all plugins (/plugins)
- Allow installing plugins from directory (/plugins)
- Allow uninstallation of plugins (/plugins uninstall)
- Allow update of plugins (/plugins update)
- Theme option for status bar time (statusbar.time)
- Case/accent insensitive autocompletion
- Shift tab to select previous autocomplete suggestion
- Allow searching help (/help search_all|search_any)
- Support for Legacy SSL
- Allow caching of rooms (/rooms cache)
- Add autocompletion for servername when listing rooms (/rooms service)
- Allow showing/disabling tab number in statusbar (/statusbar show)
- Adjust configure for OpenBSD
- Use UUIDs instead of counter for messages
- Support basic ad-hoc commands(xep-0050) (/command)
- Add option to trust server's certificate (/connect, /account)
- Add possibility to close windows via prof_win_close inputrc hook
- Bug fixes: https://github.com/profanity-im/profanity/milestone/16?closed=1

0.5.1 (2017-01-28)
=====

- Add prof.get_room_nick plugins api function
- Add main.help.header theme option
- Look for system TLS certificate path by default (/tls certpath)
- Use service discovery to set account muc property
- Allow clearing account muc and resource properties
- Allow plugins to complete file paths with prof.filepath_completer_add function
- Add encryption settings functions to plugins api
- Allow plugins to block message sending on pre message send hooks
- Fix CVE-2017-5592 (incorrect implementation of Message Carbons allowing social engineering attacks)
- Bug fixes: https://github.com/profanity-im/profanity/milestone/15?closed=1

0.5.0 (2016-09-15)
=====

- Plugins API supporting C and Python plugins
- SSL certificate verification (requires libmesode) (/tls)
- HTTP file upload (xep-0363) (/sendfile)
- Blocking command (xep-0191) (/blocked)
- Allow auto extended away (/autoaway)
- Include last acitvity in initial presence (xep-0256) (/lastactivity)
- Last Activity (xep-0012) (/lastactivity)
- Ability to run command scripts (/script)
- Account startscript property to execute a command script on connect (/account)
- Export roster to CSV file (/export)
- Support for GTK tray icons (/tray)
- User specified text triggers for chat room notifications (/notify)
- Per chat room notification options (/notify)
- Many new roster panel display options (/roster)
- Time format preferences per window type (/time)
- Edit, prepend and append to room subject (/subject)
- Autoping timeout preference (/autoping)
- Window navigation by window title (/win)
- Window closing by window title (/close)
- Account theme setting (/account)
- Allow sending XMPP stanzas in xmlconsole window (/xmlconsole)
- Configure level of room message notifications in console window (/console)
- Check ~/.config/profanity/inputrc for readline settings
- Custom readline functions for navigation key bindings
- Autocomplete command arguments when no characters entered

0.4.7 (2015-09-20)
=====

- GNU Readline
- OpenPGP support
- Message Carbons (xep-0280)
- Message Delivery Receipts (xep-0184)
- MUC Mediated Invitation support
- Configurable time formatting
- Option to show JIDs in roster
- Option to hide empty groups in roster
- Generate UUID for unnamed new MUC rooms
- Themable UI preference to indicate OTR and PGP messages
- Reformatted help
- devel: Added functional tests using libexpect and libstabber

0.4.6 (2015-03-03)
=====

- 16 colour support (/theme colours)
- UI preferences included in themes
- /wrap - Word wrapping
- /time - Show/hide time in main window, and configure precision
- /roster - Show/hide and customise roster panel
- /roster and /occupants panel size settings (% of screen width)
- /account default - Set default account for /connect
- /account remove
- /presence - Show/hide contact presence in titlebar 
- /resource - Override resource during chat, resource display settings
- Improved chat session handling <http://xmpp.org/rfcs/rfc6121.html#message-chat>
- Lower CPU usage with dynamic input blocking timeout
- Keychain/keyring integration using account eval_password property
- Disable term window title by default
- Fixed remote code execution bug on OSX when desktop notifications configured to show message text
ef='#n551'>551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622
                           
 
                                                     

                                                                 















                                                      
                                                     


















































                                                                           








































                                                                                    
                                             
                                










                                                      










































































































































































                                                                                                            




















































































































                                                                           














































































































































































































                                                                                                                     
# Helpers for decimal ints.

# if slice doesn't contain a decimal number, return 0
parse-decimal-int-from-slice:  # in: (addr slice) -> out/eax: int
    # . prologue
    55/push-ebp
    89/<- %ebp 4/r32/esp
    # . save registers
    51/push-ecx
    # ecx = in
    8b/-> *(ebp+8) 1/r32/ecx
    #
    (parse-decimal-int-helper *ecx *(ecx+4))  # => eax
$parse-decimal-int-from-slice:end:
    # . restore registers
    59/pop-to-ecx
    # . epilogue
    89/<- %esp 5/r32/ebp
    5d/pop-to-ebp
    c3/return

# if slice doesn't contain a decimal number, return 0
parse-decimal-int:  # in: (addr array byte) -> result/eax: int
    # . prologue
    55/push-ebp
    89/<- %ebp 4/r32/esp
    # . save registers
    51/push-ecx
    52/push-edx
    # eax = in
    8b/-> *(ebp+8) 0/r32/eax
    # var start/ecx: (addr byte) = &in->data
    8d/copy-address *(eax+4) 1/r32/ecx
    # var end/edx: (addr byte) = &in->data[in->size]
    8b/-> *eax 2/r32/edx
    8d/copy-address *(eax+edx+4) 2/r32/edx
    #
    (parse-decimal-int-helper %ecx %edx)  # => eax
$parse-decimal-int:end:
    # . restore registers
    5a/pop-to-edx
    59/pop-to-ecx
    # . epilogue
    89/<- %esp 5/r32/ebp
    5d/pop-to-ebp
    c3/return

parse-decimal-int-from-stream:  # in: (addr stream byte) -> result/eax: int
    # . prologue
    55/push-ebp
    89/<- %ebp 4/r32/esp
    # . save registers
    51/push-ecx
    52/push-edx
    # eax = in
    8b/-> *(ebp+8) 0/r32/eax
    # var start/ecx: (addr byte) = &in->data[in->read]
    8b/-> *(eax+4) 1/r32/ecx
    8d/copy-address *(eax+ecx+0xc) 1/r32/ecx
    # var end/edx: (addr byte) = &in->data[in->write]
    8b/-> *eax 2/r32/edx
    8d/copy-address *(eax+edx+0xc) 2/r32/edx
    #
    (parse-decimal-int-helper %ecx %edx)  # => eax
$parse-decimal-int-from-stream:end:
    # . restore registers
    5a/pop-to-edx
    59/pop-to-ecx
    # . epilogue
    89/<- %esp 5/r32/ebp
    5d/pop-to-ebp
    c3/return

parse-decimal-int-helper:  # start: (addr byte), end: (addr byte) -> result/eax: int
    # . prologue
    55/push-ebp
    89/<- %ebp 4/r32/esp
    # . save registers
    51/push-ecx
    52/push-edx
    53/push-ebx
    56/push-esi
    57/push-edi
    # var curr/esi: (addr byte) = start
    8b/-> *(ebp+8) 6/r32/esi
    # edi = end
    8b/-> *(ebp+0xc) 7/r32/edi
    # var negate?/edx: boolean = false
    ba/copy-to-edx 0/imm32/false
    # if (*curr == '-') ++curr, negate = true
    {
$parse-decimal-int-helper:negative:
      b8/copy-to-eax 0/imm32
      8a/copy-byte *esi 0/r32/AL
      3d/compare-eax-and 0x2d/imm32/-
      75/jump-if-!= break/disp8
      # . ++curr
      46/increment-esi
      # . negate = true
      ba/copy-to-edx  1/imm32/true
    }
    # spill negate?
    52/push-edx
    # var result/eax: int = 0
    b8/copy-to-eax 0/imm32
    # var digit/ecx: int = 0
    b9/copy-to-ecx 0/imm32
    # const TEN/ebx: int = 10
    bb/copy-to-ebx 0xa/imm32
    {
$parse-decimal-int-helper:loop:
      # if (curr >= in->end) break
      39/compare %esi 7/r32/edi
      73/jump-if-addr>= break/disp8
      # if !is-decimal-digit?(*curr) return 0
      8a/copy-byte *esi 1/r32/CL
      50/push-eax
      (is-decimal-digit? %ecx)  # => eax
      {
        3d/compare-eax-and 0/imm32/false
        75/jump-if-!= break/disp8
        58/pop-to-eax
        b8/copy-to-eax 0/imm32
        eb/jump $parse-decimal-int-helper:negate/disp8
      }
      58/pop-to-eax
      # digit = from-decimal-char(*curr)
      81 5/subop/subtract %ecx 0x30/imm32/zero
      # TODO: error checking
      # result = result * 10 + digit
      ba/copy-to-edx 0/imm32
      f7 4/subop/multiply-into-edx-eax %ebx
      # TODO: check edx for overflow
      01/add %eax 1/r32/ecx
      # ++curr
      46/increment-esi
      #
      eb/jump loop/disp8
    }
$parse-decimal-int-helper:negate:
    # if (negate?) result = -result
    5a/pop-to-edx
    {
      81 7/subop/compare %edx 0/imm32/false
      74/jump-if-= break/disp8
      f7 3/subop/negate %eax
    }
$parse-decimal-int-helper:end:
    # . restore registers
    5f/pop-to-edi
    5e/pop-to-esi
    5b/pop-to-ebx
    5a/pop-to-edx
    59/pop-to-ecx
    # . epilogue
    89/<- %esp 5/r32/ebp
    5d/pop-to-ebp
    c3/return

test-parse-decimal-int-from-slice-single-digit:
    # . prologue
    55/push-ebp
    89/<- %ebp 4/r32/esp
    # . save registers
    50/push-eax
    51/push-ecx
    # (eax..ecx) = "3"
    b8/copy-to-eax "3"/imm32
    8b/-> *eax 1/r32/ecx
    8d/copy-address *(eax+ecx+4) 1/r32/ecx
    05/add-to-eax 4/imm32
    # var slice/ecx: slice = {eax, ecx}
    51/push-ecx
    50/push-eax
    89/<- %ecx 4/r32/esp
    #
    (parse-decimal-int-from-slice %ecx)  # => eax
    (check-ints-equal %eax 3 "F - test-parse-decimal-int-from-slice-single-digit")
$test-parse-decimal-int-helper-single-digit:end:
    # . restore registers
    59/pop-to-ecx
    58/pop-to-eax
    # . epilogue
    89/<- %esp 5/r32/ebp
    5d/pop-to-ebp
    c3/return

test-parse-decimal-int-from-slice-multi-digit:
    # . prologue
    55/push-ebp
    89/<- %ebp 4/r32/esp
    # . save registers
    50/push-eax
    51/push-ecx
    # (eax..ecx) = "34"
    b8/copy-to-eax "34"/imm32
    8b/-> *eax 1/r32/ecx
    8d/copy-address *(eax+ecx+4) 1/r32/ecx
    05/add-to-eax 4/imm32
    # var slice/ecx: slice = {eax, ecx}
    51/push-ecx
    50/push-eax
    89/<- %ecx 4/r32/esp
    #
    (parse-decimal-int-from-slice %ecx)  # => eax
    (check-ints-equal %eax 0x22 "F - test-parse-decimal-int-from-slice-multi-digit")  # 34 in hex
$test-parse-decimal-int-helper-multi-digit:end:
    # . restore registers
    59/pop-to-ecx
    58/pop-to-eax
    # . epilogue
    89/<- %esp 5/r32/ebp
    5d/pop-to-ebp
    c3/return

test-parse-decimal-int-from-slice-zero:
    # . prologue
    55/push-ebp
    89/<- %ebp 4/r32/esp
    # . save registers
    50/push-eax
    51/push-ecx
    # (eax..ecx) = "00"
    b8/copy-to-eax "00"/imm32
    8b/-> *eax 1/r32/ecx
    8d/copy-address *(eax+ecx+4) 1/r32/ecx
    05/add-to-eax 4/imm32
    # var slice/ecx: slice = {eax, ecx}
    51/push-ecx
    50/push-eax
    89/<- %ecx 4/r32/esp
    #
    (parse-decimal-int-from-slice %ecx)  # => eax
    (check-ints-equal %eax 0 "F - test-parse-decimal-int-from-slice-zero")
$test-parse-decimal-int-helper-zero:end:
    # . restore registers
    59/pop-to-ecx
    58/pop-to-eax
    # . epilogue
    89/<- %esp 5/r32/ebp
    5d/pop-to-ebp
    c3/return

test-parse-decimal-int-from-slice-negative:
    # . prologue
    55/push-ebp
    89/<- %ebp 4/r32/esp
    # . save registers
    50/push-eax
    51/push-ecx
    # (eax..ecx) = "-3"
    b8/copy-to-eax "-3"/imm32
    8b/-> *eax 1/r32/ecx
    8d/copy-address *(eax+ecx+4) 1/r32/ecx
    05/add-to-eax 4/imm32
    # var slice/ecx: slice = {eax, ecx}
    51/push-ecx
    50/push-eax
    89/<- %ecx 4/r32/esp
    #
    (parse-decimal-int-from-slice %ecx)  # => eax
    (check-ints-equal %eax -3 "F - test-parse-decimal-int-from-slice-negative")
$test-parse-decimal-int-helper-negative:end:
    # . restore registers
    59/pop-to-ecx
    58/pop-to-eax
    # . epilogue
    89/<- %esp 5/r32/ebp
    5d/pop-to-ebp
    c3/return

test-parse-decimal-int-from-slice-multi-digit-negative:
    # . prologue
    55/push-ebp
    89/<- %ebp 4/r32/esp
    # . save registers
    50/push-eax
    51/push-ecx
    # (eax..ecx) = "-32"
    b8/copy-to-eax "-32"/imm32
    8b/-> *eax 1/r32/ecx
    8d/copy-address *(eax+ecx+4) 1/r32/ecx
    05/add-to-eax 4/imm32
    # var slice/ecx: slice = {eax, ecx}
    51/push-ecx
    50/push-eax
    89/<- %ecx 4/r32/esp
    #
    (parse-decimal-int-from-slice %ecx)  # => eax
    (check-ints-equal %eax -0x20 "F - test-parse-decimal-int-from-slice-multi-digit-negative")  # -32 in hex
$test-parse-decimal-int-helper-multi-digit-negative:end:
    # . restore registers
    59/pop-to-ecx
    58/pop-to-eax
    # . epilogue
    89/<- %esp 5/r32/ebp
    5d/pop-to-ebp
    c3/return

decimal-size:  # n: int -> result/eax: int
    # pseudocode:
    #   edi = 0
    #   eax = n
    #   if eax < 0
    #     ++edi  # for '-'
    #     negate eax
    #   while true
    #     edx = 0
    #     eax, edx = eax/10, eax%10
    #     ++edi
    #     if (eax == 0) break
    #   eax = edi
    #
    # . prologue
    55/push-ebp
    89/<- %ebp 4/r32/esp
    # . save registers
    51/push-ecx
    52/push-edx
    57/push-edi
    # edi = 0
    bf/copy-to-edi 0/imm32
    # eax = n
    8b/-> *(ebp+8) 0/r32/eax
    # if (n < 0) negate n, increment edi
    {
      3d/compare-eax-with 0/imm32
      7d/jump-if->= break/disp8
      f7 3/subop/negate %eax
      47/increment-edi
    }
    # const ten/ecx = 10
    b9/copy-to-ecx  0xa/imm32
    {
      ba/copy-to-edx 0/imm32
      f7 7/subop/idiv-edx-eax-by %ecx  # eax = edx:eax/10; edx = edx:eax%10
      47/increment-edi
      3d/compare-eax-and 0/imm32
      75/jump-if-!= loop/disp8
    }
$decimal-size:end:
    89/<- %eax 7/r32/edi
    # . restore registers
    5f/pop-to-edi
    5a/pop-to-edx
    59/pop-to-ecx
    # . epilogue
    89/<- %esp 5/r32/ebp
    5d/pop-to-ebp
    c3/return

test-decimal-size-of-zero:
    # . prologue
    55/push-ebp
    89/<- %ebp 4/r32/esp
    #
    (decimal-size 0)  # => eax
    (check-ints-equal %eax 1 "F - test-decimal-size-of-zero")
$test-decimal-size-of-zero:end:
    # . epilogue
    89/<- %esp 5/r32/ebp
    5d/pop-to-ebp
    c3/return

test-decimal-size-single-digit:
    # . prologue
    55/push-ebp
    89/<- %ebp 4/r32/esp
    #
    (decimal-size 4)  # => eax
    (check-ints-equal %eax 1 "F - test-decimal-size-single-digit")
$test-decimal-size-single-digit:end:
    # . epilogue
    89/<- %esp 5/r32/ebp
    5d/pop-to-ebp
    c3/return

test-decimal-size-multi-digit:
    # . prologue
    55/push-ebp
    89/<- %ebp 4/r32/esp
    #
    (decimal-size 0xa)  # => eax
    (check-ints-equal %eax 2 "F - test-decimal-size-multi-digit")
$test-decimal-size-multi-digit:end:
    # . epilogue
    89/<- %esp 5/r32/ebp
    5d/pop-to-ebp
    c3/return

test-decimal-size-single-digit-negative:
    # . prologue
    55/push-ebp
    89/<- %ebp 4/r32/esp
    #
    (decimal-size -4)  # => eax
    (check-ints-equal %eax 2 "F - test-decimal-size-single-digit-negative")
$test-decimal-size-single-digit-negative:end:
    # . epilogue
    89/<- %esp 5/r32/ebp
    5d/pop-to-ebp
    c3/return

test-decimal-size-multi-digit-negative:
    # . prologue
    55/push-ebp
    89/<- %ebp 4/r32/esp
    #
    (decimal-size -0xa)  # => eax
    (check-ints-equal %eax 3 "F - test-decimal-size-multi-digit-negative")
$test-decimal-size-multi-digit-negative:end:
    # . epilogue
    89/<- %esp 5/r32/ebp
    5d/pop-to-ebp
    c3/return

_parse-array-of-decimal-ints:  # ad: (addr allocation-descriptor), s: (addr array byte), out: (addr handle array int)
    # pseudocode
    #   end = &s->data[s->size]
    #   curr = s->data
    #   size = 0
    #   while true
    #     if (curr >= end) break
    #     curr = skip-chars-matching-in-slice(curr, end, ' ')
    #     if (curr >= end) break
    #     curr = skip-chars-not-matching-in-slice(curr, end, ' ')
    #     ++size
    #   allocate-array(ad, size*4, out)
    #   var slice: slice = {s->data, 0}
    #   curr = lookup(out)->data
    #   while true
    #     if (slice->start >= end) break
    #     slice->start = skip-chars-matching-in-slice(slice->start, end, ' ')
    #     if (slice->start >= end) break
    #     slice->end = skip-chars-not-matching-in-slice(slice->start, end, ' ')
    #     *curr = parse-hex-int-from-slice(slice)
    #     curr += 4
    #     slice->start = slice->end
    #   return result
    #
    # . prologue
    55/push-ebp
    89/<- %ebp 4/r32/esp
    # . save registers
    50/push-eax
    51/push-ecx
    52/push-edx
    53/push-ebx
    56/push-esi
    57/push-edi
    # esi = s
    8b/-> *(ebp+0xc) 6/r32/esi
    # var curr/ecx: (addr byte) = s->data
    8d/copy-address *(esi+4) 1/r32/ecx
    # var end/edx: (addr byte) = &s->data[s->size]
    # . edx = s->size
    8b/-> *esi 2/r32/edx
    # . edx += curr
    01/add-to %edx 1/r32/ecx
    # var size/ebx: int = 0
    31/xor-with %ebx 3/r32/ebx
$_parse-array-of-decimal-ints:loop1:
    # if (curr >= end) break
    39/compare %ecx 2/r32/edx
    73/jump-if-addr>= $_parse-array-of-decimal-ints:break1/disp8
    # curr = skip-chars-matching-in-slice(curr, end, ' ')
    (skip-chars-matching-in-slice %ecx %edx 0x20)  # => eax
    89/<- %ecx 0/r32/eax
    # if (curr >= end) break
    39/compare %ecx 2/r32/edx
    73/jump-if-addr>= $_parse-array-of-decimal-ints:break1/disp8
    # curr = skip-chars-not-matching-in-slice(curr, end, ' ')
    (skip-chars-not-matching-in-slice %ecx %edx 0x20)  # => eax
    89/<- %ecx 0/r32/eax
    # size += 4
    81 0/subop/add %ebx 4/imm32
    eb/jump $_parse-array-of-decimal-ints:loop1/disp8
$_parse-array-of-decimal-ints:break1:
    (allocate-array *(ebp+8) %ebx *(ebp+0x10))
$_parse-array-of-decimal-ints:pass2:
    # var slice/edi: slice = {s->data, 0}
    68/push 0/imm32/end
    8d/copy-address *(esi+4) 7/r32/edi
    57/push-edi
    89/<- %edi 4/r32/esp
    # curr = lookup(out)->data
    8b/-> *(ebp+0x10) 0/r32/eax
    (lookup *eax *(eax+4))  # => eax
    8d/copy-address *(eax+4) 1/r32/ecx
$_parse-array-of-decimal-ints:loop2:
    # if (slice->start >= end) break
    39/compare *edi 2/r32/edx
    73/jump-if-addr>= $_parse-array-of-decimal-ints:end/disp8
    # slice->start = skip-chars-matching-in-slice(slice->start, end, ' ')
    (skip-chars-matching-in-slice *edi %edx 0x20)  # => eax
    89/<- *edi 0/r32/eax
    # if (slice->start >= end) break
    39/compare *edi 2/r32/edx
    73/jump-if-addr>= $_parse-array-of-decimal-ints:end/disp8
    # slice->end = skip-chars-not-matching-in-slice(slice->start, end, ' ')
    (skip-chars-not-matching-in-slice *edi %edx 0x20)  # => eax
    89/<- *(edi+4) 0/r32/eax
    # *curr = parse-hex-int-from-slice(slice)
    (parse-decimal-int-from-slice %edi)
    89/<- *ecx 0/r32/eax
    # curr += 4
    81 0/subop/add %ecx 4/imm32
    # slice->start = slice->end
    8b/-> *(edi+4) 0/r32/eax
    89/<- *edi 0/r32/eax
    eb/jump $_parse-array-of-decimal-ints:loop2/disp8
$_parse-array-of-decimal-ints:end:
    # . reclaim locals
    81 0/subop/add %esp 8/imm32
    # . restore registers
    5f/pop-to-edi
    5e/pop-to-esi
    5b/pop-to-ebx
    5a/pop-to-edx
    59/pop-to-ecx
    58/pop-to-eax
    # . epilogue
    89/<- %esp 5/r32/ebp
    5d/pop-to-ebp
    c3/return

test-parse-array-of-decimal-ints:
    # . prologue
    55/push-ebp
    89/<- %ebp 4/r32/esp
    # var h/esi: (handle array int)
    68/push 0/imm32
    68/push 0/imm32
    89/<- %esi 4/r32/esp
    # var ecx: (array int) = [1, 2, 3]
    68/push 3/imm32
    68/push 2/imm32
    68/push 1/imm32
    68/push 0xc/imm32/size
    89/<- %ecx 4/r32/esp
    #
    (_parse-array-of-decimal-ints Heap "1 2 3" %esi)
    (lookup *esi *(esi+4))  # => eax
    (array-equal? %ecx %eax)  # => eax
    (check-ints-equal %eax 1 "F - test-parse-array-of-decimal-ints")
    # . epilogue
    89/<- %esp 5/r32/ebp
    5d/pop-to-ebp
    c3/return

test-parse-array-of-decimal-ints-empty:
    # - empty string = empty array
    # . prologue
    55/push-ebp
    89/<- %ebp 4/r32/esp
    # var h/esi: handle
    68/push 0/imm32
    68/push 0/imm32
    89/<- %esi 4/r32/esp
    #
    (_parse-array-of-decimal-ints Heap "" %esi)
    (lookup *esi *(esi+4))  # => eax
    (check-ints-equal *eax 0 "F - test-parse-array-of-decimal-ints-empty")
    # . epilogue
    89/<- %esp 5/r32/ebp
    5d/pop-to-ebp
    c3/return

test-parse-array-of-decimal-ints-just-whitespace:
    # - just whitespace = empty array
    # . prologue
    55/push-ebp
    89/<- %ebp 4/r32/esp
    # var h/esi: handle
    68/push 0/imm32
    68/push 0/imm32
    89/<- %esi 4/r32/esp
    #
    (_parse-array-of-decimal-ints Heap Space %esi)
    (lookup *esi *(esi+4))  # => eax
    (check-ints-equal *eax 0 "F - test-parse-array-of-decimal-ints-just-whitespace")
    # . epilogue
    89/<- %esp 5/r32/ebp
    5d/pop-to-ebp
    c3/return

test-parse-array-of-decimal-ints-extra-whitespace:
    # . prologue
    55/push-ebp
    89/<- %ebp 4/r32/esp
    # var h/esi: handle
    68/push 0/imm32
    68/push 0/imm32
    89/<- %esi 4/r32/esp
    # var ecx: (array int) = [1, 2, 3]
    68/push 3/imm32
    68/push 2/imm32
    68/push 1/imm32
    68/push 0xc/imm32/size
    89/<- %ecx 4/r32/esp
    #
    (_parse-array-of-decimal-ints Heap " 1 2  3  " %esi)
    (lookup *esi *(esi+4))  # => eax
    (array-equal? %ecx %eax)  # => eax
    (check-ints-equal %eax 1 "F - test-parse-array-of-decimal-ints-extra-whitespace")
    # . epilogue
    89/<- %esp 5/r32/ebp
    5d/pop-to-ebp
    c3/return

parse-array-of-decimal-ints:  # s: (addr array byte), out: (addr handle array int)
    # . prologue
    55/push-ebp
    89/<- %ebp 4/r32/esp
    #
    (_parse-array-of-decimal-ints Heap *(ebp+8) *(ebp+0xc))
$parse-array-of-decimal-ints:end:
    # . epilogue
    89/<- %esp 5/r32/ebp
    5d/pop-to-ebp
    c3/return