1 /**
2  * @file
3  * @brief Functions that may be missing from some systems
4 **/
5 
6 #include "AppHdr.h"
7 
8 #include "libutil.h"
9 
10 #include <cctype>
11 #include <cstdarg>
12 #include <cstdio>
13 #include <cstring>
14 #include <sstream>
15 
16 #include "colour.h"
17 #include "files.h"
18 #include "message.h"
19 #include "state.h"
20 #include "stringutil.h"
21 #include "tiles-build-specific.h"
22 #include "unicode.h"
23 #include "viewgeom.h"
24 
25 #ifdef TARGET_OS_WINDOWS
26     #undef ARRAYSZ
27     #include <windows.h>
28     #undef max
29 
30     #ifdef WINMM_PLAY_SOUNDS
31         #include <mmsystem.h>
32     #endif
33 #endif
34 
35 #ifdef UNIX
36     #include <csignal>
37 #endif
38 
39 #ifdef DGL_ENABLE_CORE_DUMP
40     #include <sys/time.h>
41     #include <sys/resource.h>
42 #endif
43 
isqrt(unsigned int a)44 unsigned int isqrt(unsigned int a)
45 {
46     unsigned int rem = 0, root = 0;
47     for (int i = 0; i < 16; i++)
48     {
49         root <<= 1;
50         rem = (rem << 2) + (a >> 30);
51         a <<= 2;
52         if (++root <= rem)
53             rem -= root++;
54         else
55             root--;
56     }
57     return root >> 1;
58 }
59 
isqrt_ceil(int x)60 int isqrt_ceil(int x)
61 {
62     if (x <= 0)
63         return 0;
64     return isqrt(x - 1) + 1;
65 }
66 
description_type_by_name(const char * desc)67 description_level_type description_type_by_name(const char *desc)
68 {
69     if (!desc)
70         return DESC_PLAIN;
71 
72     if (!strcmp("The", desc) || !strcmp("the", desc))
73         return DESC_THE;
74     else if (!strcmp("A", desc) || !strcmp("a", desc))
75         return DESC_A;
76     else if (!strcmp("Your", desc) || !strcmp("your", desc))
77         return DESC_YOUR;
78     else if (!strcmp("its", desc))
79         return DESC_ITS;
80     else if (!strcmp("worn", desc))
81         return DESC_INVENTORY_EQUIP;
82     else if (!strcmp("inv", desc))
83         return DESC_INVENTORY;
84     else if (!strcmp("none", desc))
85         return DESC_NONE;
86     else if (!strcmp("base", desc))
87         return DESC_BASENAME;
88     else if (!strcmp("qual", desc))
89         return DESC_QUALNAME;
90 
91     return DESC_PLAIN;
92 }
93 
94 // Should return true if the filename contains nothing that
95 // the shell can do damage with.
shell_safe(const char * file)96 bool shell_safe(const char *file)
97 {
98     int match = strcspn(file, "\\`$*?|><&\n!;");
99     return match < 0 || !file[match];
100 }
101 
key_is_escape(int key)102 bool key_is_escape(int key)
103 {
104     switch (key)
105     {
106     CASE_ESCAPE
107         return true;
108     default:
109         return false;
110     }
111 }
112 
113 // Returns true if s contains tag 'tag', and strips out tag from s.
strip_tag(string & s,const string & tag,bool skip_padding)114 bool strip_tag(string &s, const string &tag, bool skip_padding)
115 {
116     if (s == tag)
117     {
118         s.clear();
119         return true;
120     }
121 
122     string::size_type pos;
123 
124     if (skip_padding)
125     {
126         if ((pos = s.find(tag)) != string::npos)
127         {
128             s.erase(pos, tag.length());
129             trim_string(s);
130             return true;
131         }
132         return false;
133     }
134 
135     if ((pos = s.find(" " + tag + " ")) != string::npos)
136     {
137         // Leave one space intact.
138         s.erase(pos, tag.length() + 1);
139         trim_string(s);
140         return true;
141     }
142 
143     if ((pos = s.find(tag + " ")) == 0
144         || ((pos = s.find(" " + tag)) != string::npos
145             && pos + tag.length() + 1 == s.length()))
146     {
147         s.erase(pos, tag.length() + 1);
148         trim_string(s);
149         return true;
150     }
151 
152     return false;
153 }
154 
strip_multiple_tag_prefix(string & s,const string & tagprefix)155 vector<string> strip_multiple_tag_prefix(string &s, const string &tagprefix)
156 {
157     vector<string> results;
158 
159     while (true)
160     {
161         string this_result = strip_tag_prefix(s, tagprefix);
162         if (this_result.empty())
163             break;
164 
165         results.push_back(this_result);
166     }
167 
168     return results;
169 }
170 
strip_tag_prefix(string & s,const string & tagprefix)171 string strip_tag_prefix(string &s, const string &tagprefix)
172 {
173     string::size_type pos = s.find(tagprefix);
174 
175     while (pos && pos != string::npos && !isspace(s[pos - 1]))
176         pos = s.find(tagprefix, pos + 1);
177 
178     if (pos == string::npos)
179         return "";
180 
181     string::size_type ns = s.find(" ", pos);
182     if (ns == string::npos)
183         ns = s.length();
184 
185     const string argument = s.substr(pos + tagprefix.length(),
186                                      ns - pos - tagprefix.length());
187 
188     s.erase(pos, ns - pos + 1);
189     trim_string(s);
190 
191     return argument;
192 }
193 
tag_without_prefix(const string & s,const string & tagprefix)194 const string tag_without_prefix(const string &s, const string &tagprefix)
195 {
196     string::size_type pos = s.find(tagprefix);
197 
198     while (pos && pos != string::npos && !isspace(s[pos - 1]))
199         pos = s.find(tagprefix, pos + 1);
200 
201     if (pos == string::npos)
202         return "";
203 
204     string::size_type ns = s.find(" ", pos);
205     if (ns == string::npos)
206         ns = s.length();
207 
208     return s.substr(pos + tagprefix.length(), ns - pos - tagprefix.length());
209 }
210 
strip_number_tag(string & s,const string & tagprefix)211 int strip_number_tag(string &s, const string &tagprefix)
212 {
213     const string num = strip_tag_prefix(s, tagprefix);
214     int x;
215     if (num.empty() || !parse_int(num.c_str(), x))
216         return TAG_UNFOUND;
217     return x;
218 }
219 
parse_tags(const string & tags)220 unordered_set<string> parse_tags(const string &tags)
221 {
222     vector<string> split_tags = split_string(" ", tags);
223     unordered_set<string> tags_set(split_tags.begin(), split_tags.end());
224     return tags_set;
225 }
226 
parse_int(const char * s,int & i)227 bool parse_int(const char *s, int &i)
228 {
229     if (!s || !*s)
230         return false;
231     char *err;
232     long x = strtol(s, &err, 10);
233     if (*err || x < INT_MIN || x > INT_MAX)
234         return false;
235     i = x;
236     return true;
237 }
238 
239 /**
240  * Compare two strings, sorting integer numeric parts according to their value.
241  *
242  * "foo123bar" > "foo99bar"
243  * "0.10" > "0.9" (version sort)
244  *
245  * @param a String one.
246  * @param b String two.
247  * @param limit If passed, comparison ends after X numeric parts.
248  * @return As in strcmp().
249 **/
numcmp(const char * a,const char * b,int limit)250 int numcmp(const char *a, const char *b, int limit)
251 {
252     int res;
253 
254     do
255     {
256         while (*a && *a == *b && !isadigit(*a))
257         {
258             a++;
259             b++;
260         }
261         if (!isadigit(*a) || !isadigit(*b))
262             return (*a < *b) ? -1 : (*a > *b) ? 1 : 0;
263         while (*a == '0')
264             a++;
265         while (*b == '0')
266             b++;
267         res = 0;
268         while (isadigit(*a))
269         {
270             if (!isadigit(*b))
271                 return 1;
272             if (*a != *b && !res)
273                 res = (*a < *b) ? -1 : 1;
274             a++;
275             b++;
276         }
277         if (isadigit(*b))
278             return -1;
279         if (res)
280             return res;
281     } while (--limit > 0);
282     return 0;
283 }
284 
285 // make STL sort happy
numcmpstr(const string & a,const string & b)286 bool numcmpstr(const string& a, const string& b)
287 {
288     return numcmp(a.c_str(), b.c_str()) == -1;
289 }
290 
version_is_stable(const char * v)291 bool version_is_stable(const char *v)
292 {
293     // vulnerable to changes in the versioning scheme
294     for (;; v++)
295     {
296         if (*v == '.' || isadigit(*v))
297             continue;
298         if (*v == '-')
299             return isadigit(v[1]);
300         return true;
301     }
302 }
303 
_untag(string & s,const string & pre,const string & post,bool onoff)304 static void inline _untag(string &s, const string &pre,
305                           const string &post, bool onoff)
306 {
307     size_t p = 0;
308     while ((p = s.find(pre, p)) != string::npos)
309     {
310         size_t q = s.find(post, p);
311         if (q == string::npos)
312             q = s.length();
313         if (onoff)
314         {
315             s.erase(q, post.length());
316             s.erase(p, pre.length());
317         }
318         else
319             s.erase(p, q - p + post.length());
320     }
321 }
322 
untag_tiles_console(string s)323 string untag_tiles_console(string s)
324 {
325     _untag(s, "<tiles>", "</tiles>", is_tiles());
326     _untag(s, "<console>", "</console>", !is_tiles());
327 #ifdef USE_TILE_WEB
328     _untag(s, "<webtiles>", "</webtiles>", true);
329 #else
330     _untag(s, "<webtiles>", "</webtiles>", false);
331 #endif
332 #ifdef USE_TILE_LOCAL
333     _untag(s, "<localtiles>", "</localtiles>", true);
334     _untag(s, "<nomouse>", "</nomouse>", false);
335 #else
336     _untag(s, "<localtiles>", "</localtiles>", false);
337     _untag(s, "<nomouse>", "</nomouse>", true);
338 #endif
339     return s;
340 }
341 
colour_string(string in,int col)342 string colour_string(string in, int col)
343 {
344     if (in.empty())
345         return in;
346     const string cols = colour_to_str(col);
347     return "<" + cols + ">" + in + "</" + cols + ">";
348 }
349 
350 
351 
352 #ifndef USE_TILE_LOCAL
_cgettopleft(GotoRegion region)353 static coord_def _cgettopleft(GotoRegion region)
354 {
355     switch (region)
356     {
357     case GOTO_MLIST:
358         return crawl_view.mlistp;
359     case GOTO_STAT:
360         return crawl_view.hudp;
361     case GOTO_MSG:
362         return crawl_view.msgp;
363     case GOTO_DNGN:
364         return crawl_view.viewp;
365     case GOTO_CRT:
366     default:
367         return crawl_view.termp;
368     }
369 }
370 
cgetpos(GotoRegion region)371 coord_def cgetpos(GotoRegion region)
372 {
373     const coord_def where = coord_def(wherex(), wherey());
374     return where - _cgettopleft(region) + coord_def(1, 1);
375 }
376 
valid_cursor_pos(int x,int y,GotoRegion region)377 bool valid_cursor_pos(int x, int y, GotoRegion region)
378 {
379     const coord_def sz = cgetsize(region);
380     return x >= 1 && y >= 1 && x <= sz.x && y <= sz.y;
381 }
382 
383 #ifndef TARGET_OS_WINDOWS
_find_correct_region()384 static GotoRegion _find_correct_region()
385 {
386     for (int r = GOTO_MSG; r < GOTO_MLIST; r++)
387     {
388         GotoRegion region(static_cast<GotoRegion>(r));
389         coord_def pos(cgetpos(region));
390         if (valid_cursor_pos(pos.x, pos.y, region))
391             return region;
392     }
393     // everything on screen is valid in GOTO_CRT; though in principle this
394     // should probably fail for entirely invalid coords somehow
395     return GOTO_CRT;
396 }
397 #endif
398 
assert_valid_cursor_pos()399 void assert_valid_cursor_pos()
400 {
401 #ifndef TARGET_OS_WINDOWS
402     GotoRegion region(get_cursor_region());
403     coord_def pos(cgetpos(region));
404     ASSERTM(valid_cursor_pos(pos.x, pos.y, region),
405         "invalid cursor position %d,%d in region %d, should be %d,%d in region %d",
406         pos.x, pos.y, region, cgetpos(_find_correct_region()).x,
407         cgetpos(_find_correct_region()).y, _find_correct_region());
408 #endif
409 }
410 
411 static GotoRegion _current_region = GOTO_CRT;
412 
cgotoxy(int x,int y,GotoRegion region)413 void cgotoxy(int x, int y, GotoRegion region)
414 {
415 #if defined(ASSERTS) && !defined(TARGET_OS_WINDOWS)
416     if (!valid_cursor_pos(x, y, region))
417     {
418         const coord_def sz = cgetsize(region);
419         // dprf in case it's not safe
420         dprf("screen write out of bounds in region %d (old: %d): (%d,%d) into (%d,%d)",
421              (int) region, (int) _current_region, x, y, sz.x, sz.y);
422         if (you.on_current_level)
423             save_game(false); // should be safe (lol)
424         die( "screen write out of bounds in region %d (old: %d): (%d,%d) into (%d,%d)",
425              (int) region, (int) _current_region, x, y, sz.x, sz.y);
426     }
427 #endif
428 
429     const coord_def tl = _cgettopleft(region);
430     _current_region = region;
431     gotoxy_sys(tl.x + x - 1, tl.y + y - 1);
432 
433 #ifdef USE_TILE_WEB
434     tiles.cgotoxy(x, y, region);
435 #endif
436 }
437 
get_cursor_region()438 GotoRegion get_cursor_region()
439 {
440     return _current_region;
441 }
442 
set_cursor_region(GotoRegion region)443 void set_cursor_region(GotoRegion region)
444 {
445     _current_region = region;
446 }
447 
clrscr()448 void clrscr()
449 {
450     save_cursor_pos save; // is this generally correct? alternative, set region
451                           // to GOTO_CRT.
452     clrscr_sys();
453 
454 #ifdef USE_TILE_WEB
455     tiles.clrscr();
456 #endif
457 }
458 
459 #endif // !USE_TILE_LOCAL
460 
cgetsize(GotoRegion region)461 coord_def cgetsize(GotoRegion region)
462 {
463     switch (region)
464     {
465     case GOTO_MLIST:
466         return crawl_view.mlistsz;
467     case GOTO_STAT:
468         return crawl_view.hudsz;
469     case GOTO_MSG:
470         return crawl_view.msgsz;
471     case GOTO_DNGN:
472         return crawl_view.viewsz;
473     case GOTO_CRT:
474     default:
475         return crawl_view.termsz;
476     }
477 }
478 
cscroll(int n,GotoRegion region)479 void cscroll(int n, GotoRegion region)
480 {
481     // only implemented for the message window right now
482     if (region == GOTO_MSG)
483         scroll_message_window(n);
484 }
485 
486 mouse_mode mouse_control::ms_current_mode = MOUSE_MODE_NORMAL;
487 
mouse_control(mouse_mode mode)488 mouse_control::mouse_control(mouse_mode mode)
489 {
490 #ifdef USE_TILE_WEB
491     // relies on being called before ms_current_mode is updated
492     tiles.update_input_mode(mode);
493 #endif
494 
495     m_previous_mode = ms_current_mode;
496     ms_current_mode = mode;
497 }
498 
~mouse_control()499 mouse_control::~mouse_control()
500 {
501 #ifdef USE_TILE_WEB
502     tiles.update_input_mode(m_previous_mode);
503 #endif
504     ms_current_mode = m_previous_mode;
505 }
506 
unwrap_desc(string && desc)507 string unwrap_desc(string&& desc)
508 {
509     // Don't append a newline to an empty description.
510     if (desc.empty())
511         return "";
512 
513     trim_string_right(desc);
514 
515     // Lines beginning with a colon are tags with a full entry scope
516     // Only nowrap is supported for now
517     while (desc[0] == ':')
518     {
519         int pos = desc.find("\n");
520         string tag = desc.substr(1, pos - 1);
521         desc.erase(0, pos + 1);
522         if (tag == "nowrap")
523             return move(desc);
524         else if (desc.empty())
525             return "";
526     }
527 
528     // An empty line separates paragraphs.
529     desc = replace_all(desc, "\n\n", "\\n\\n");
530     // Indented lines are pre-formatted.
531     desc = replace_all(desc, "\n ", "\\n ");
532 
533     // Don't add whitespaces between tags
534     desc = replace_all(desc, ">\n<", "><");
535     // Newlines are still whitespace.
536     desc = replace_all(desc, "\n", " ");
537     // Can force a newline with a literal "\n".
538     desc = replace_all(desc, "\\n", "\n");
539 
540     return desc + "\n";
541 }
542 
543 #ifdef TARGET_OS_WINDOWS
544 // FIXME: This function should detect if aero is running, but the DwmIsCompositionEnabled
545 // function isn't included in msys, so I don't know how to do that. Instead, I just check
546 // if we are running vista or higher. -rla
_is_aero()547 static bool _is_aero()
548 {
549     OSVERSIONINFOEX osvi;
550     osvi.dwOSVersionInfoSize = sizeof(OSVERSIONINFOEX);
551     if (GetVersionEx((OSVERSIONINFO *) &osvi))
552         return osvi.dwMajorVersion >= 6;
553     else
554         return false;
555 }
556 
get_taskbar_pos()557 taskbar_pos get_taskbar_pos()
558 {
559     RECT rect;
560     HWND taskbar = FindWindow("Shell_traywnd", nullptr);
561     if (taskbar && GetWindowRect(taskbar, &rect))
562     {
563         if (rect.right - rect.left > rect.bottom - rect.top)
564         {
565             if (rect.top > 0)
566                 return TASKBAR_BOTTOM;
567             else
568                 return TASKBAR_TOP;
569         }
570         else
571         {
572             if (rect.left > 0)
573                 return TASKBAR_RIGHT;
574             else
575                 return TASKBAR_LEFT;
576         }
577     }
578     return TASKBAR_NO;
579 }
580 
get_taskbar_size()581 int get_taskbar_size()
582 {
583     RECT rect;
584     int size;
585     taskbar_pos tpos = get_taskbar_pos();
586     HWND taskbar = FindWindow("Shell_traywnd", nullptr);
587 
588     if (taskbar && GetWindowRect(taskbar, &rect))
589     {
590         if (tpos & TASKBAR_H)
591             size = rect.bottom - rect.top;
592         else if (tpos & TASKBAR_V)
593             size = rect.right - rect.left;
594         else
595             return 0;
596 
597         if (_is_aero())
598             size += 3; // Taskbar behave strangely when aero is active.
599 
600         return size;
601     }
602     return 0;
603 }
604 
console_handler(DWORD sig)605 static BOOL WINAPI console_handler(DWORD sig)
606 {
607     switch (sig)
608     {
609     case CTRL_C_EVENT:
610     case CTRL_BREAK_EVENT:
611         return true; // block the signal
612     default:
613         return false;
614     case CTRL_CLOSE_EVENT:
615     case CTRL_LOGOFF_EVENT:
616     case CTRL_SHUTDOWN_EVENT:
617         if (crawl_state.seen_hups++)
618             return true; // abort immediately
619 
620 #ifndef USE_TILE_LOCAL
621         // Should never happen in tiles -- if it does (Cygwin?), this will
622         // kill the game without saving.
623         w32_insert_escape();
624 
625         Sleep(15000); // allow 15 seconds for shutdown, then kill -9
626 #endif
627         return true;
628     }
629 }
630 
init_signals()631 void init_signals()
632 {
633     // If there's no console, this will return an error, which we ignore.
634     // For GUI programs there's no controlling terminal, but there might be
635     // one on Cygwin.
636     SetConsoleCtrlHandler(console_handler, true);
637 }
638 
release_cli_signals()639 void release_cli_signals()
640 {
641     SetConsoleCtrlHandler(nullptr, false);
642 }
643 
text_popup(const string & text,const wchar_t * caption)644 void text_popup(const string& text, const wchar_t *caption)
645 {
646     MessageBoxW(0, OUTW(text), caption, MB_OK);
647 }
648 #else
649 #ifndef USE_TILE_LOCAL // is curses in use?
650 
651 /* [ds] This SIGHUP handling is primitive and far from safe, but it
652  * should be better than nothing. Feel free to get rigorous on this.
653  */
handle_hangup(int)654 static void handle_hangup(int)
655 {
656     if (crawl_state.seen_hups++)
657         return;
658 
659     // When using Curses, closing stdin will cause any Curses call blocking
660     // on key-presses to immediately return, including any call that was
661     // still blocking in the main thread when the HUP signal was caught.
662     // This should guarantee that the main thread will un-stall and
663     // will eventually return to _input() in main.cc, which will then
664     // gracefully save the game.
665 
666     // SAVE CORRUPTING BUG!!!  We're in a signal handler, calling free()
667     // when closing the FILE object is likely to lead to lock-ups, and even
668     // if it were a plain kernel-side descriptor, calling functions such
669     // as select() or read() is undefined behaviour.
670     fclose(stdin);
671 }
672 # endif
673 
init_signals()674 void init_signals()
675 {
676 #ifdef DGAMELAUNCH
677     // Force timezone to UTC.
678     setenv("TZ", "", 1);
679     tzset();
680 #endif
681 
682 #ifdef USE_UNIX_SIGNALS
683 #ifdef SIGQUIT
684     signal(SIGQUIT, SIG_IGN);
685 #endif
686 
687 #ifdef SIGINT
688     signal(SIGINT, SIG_IGN);
689 #endif
690 
691 # ifdef USE_TILE_LOCAL
692     // Losing the controlling terminal doesn't matter, we continue and will
693     // shut down only when the actual window is closed.
694     signal(SIGHUP, SIG_IGN);
695 # else
696     signal(SIGHUP, handle_hangup);
697 # endif
698 #endif
699 
700 #ifdef DGL_ENABLE_CORE_DUMP
701     rlimit lim;
702     if (!getrlimit(RLIMIT_CORE, &lim))
703     {
704         lim.rlim_cur = RLIM_INFINITY;
705         setrlimit(RLIMIT_CORE, &lim);
706     }
707 #endif
708 }
709 
release_cli_signals()710 void release_cli_signals()
711 {
712 #ifdef USE_UNIX_SIGNALS
713     signal(SIGQUIT, SIG_DFL);
714     signal(SIGINT, SIG_DFL);
715 #endif
716 }
717 
718 #endif
719