1 /*-
2 * SPDX-License-Identifier: BSD-2-Clause
3 *
4 * Copyright (c) 2001 Alexey Zelkin <phantom@FreeBSD.org>
5 * All rights reserved.
6 *
7 * Copyright (c) 2011 The FreeBSD Foundation
8 *
9 * Portions of this software were developed by David Chisnall
10 * under sponsorship from the FreeBSD Foundation.
11 *
12 * Redistribution and use in source and binary forms, with or without
13 * modification, are permitted provided that the following conditions
14 * are met:
15 * 1. Redistributions of source code must retain the above copyright
16 * notice, this list of conditions and the following disclaimer.
17 * 2. Redistributions in binary form must reproduce the above copyright
18 * notice, this list of conditions and the following disclaimer in the
19 * documentation and/or other materials provided with the distribution.
20 *
21 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
22 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
23 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
24 * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
25 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
26 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
27 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
28 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
29 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
30 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
31 * SUCH DAMAGE.
32 *
33 */
34
35 #include <sys/types.h>
36
37 #include <ctype.h>
38 #include <errno.h>
39 #include <limits.h>
40 #include <locale.h>
41 #include <monetary.h>
42 #include <stdarg.h>
43 #include <stdio.h>
44 #include <stdlib.h>
45 #include <string.h>
46
47 #include "xlocale_private.h"
48
49 /* internal flags */
50 #define NEED_GROUPING 0x01 /* print digits grouped (default) */
51 #define SIGN_POSN_USED 0x02 /* '+' or '(' usage flag */
52 #define LOCALE_POSN 0x04 /* use locale defined +/- (default) */
53 #define PARENTH_POSN 0x08 /* enclose negative amount in () */
54 #define SUPPRESS_CURR_SYMBOL 0x10 /* suppress the currency from output */
55 #define LEFT_JUSTIFY 0x20 /* left justify */
56 #define USE_INTL_CURRENCY 0x40 /* use international currency symbol */
57 #define IS_NEGATIVE 0x80 /* is argument value negative ? */
58
59 /* internal macros */
60 #define PRINT(CH) do { \
61 if (dst >= s + maxsize) \
62 goto e2big_error; \
63 *dst++ = CH; \
64 } while (0)
65
66 #define PRINTS(STR) do { \
67 char *tmps = STR; \
68 while (*tmps != '\0') \
69 PRINT(*tmps++); \
70 } while (0)
71
72 #define GET_NUMBER(VAR, LOC) do { \
73 VAR = 0; \
74 while (isdigit_l((unsigned char)*fmt, LOC)) { \
75 if (VAR > INT_MAX / 10) \
76 goto e2big_error; \
77 VAR *= 10; \
78 VAR += *fmt - '0'; \
79 if (VAR < 0) \
80 goto e2big_error; \
81 fmt++; \
82 } \
83 } while (0)
84
85 #define GRPCPY(howmany) do { \
86 int i = howmany; \
87 while (i-- > 0) { \
88 avalue_size--; \
89 *--bufend = *(avalue + avalue_size + padded); \
90 } \
91 } while (0)
92
93 #define GRPSEP do { \
94 bufend -= thousands_sep_size; \
95 memcpy(bufend, thousands_sep, thousands_sep_size); \
96 groups++; \
97 } while (0)
98
99 static void __setup_vars(int, char *, char *, char *, char **, struct lconv *);
100 static int __calc_left_pad(int, char *, struct lconv *);
101 static char *__format_grouped_double(double, int *, int, int, int,
102 struct lconv *, locale_t);
103
104 static ssize_t
vstrfmon_l(char * __restrict s,size_t maxsize,locale_t loc,const char * __restrict format,va_list ap)105 vstrfmon_l(char *__restrict s, size_t maxsize, locale_t loc,
106 const char *__restrict format, va_list ap)
107 {
108 char *dst; /* output destination pointer */
109 const char *fmt; /* current format position pointer */
110 struct lconv *lc; /* pointer to lconv structure */
111 char *asciivalue; /* formatted double pointer */
112
113 int flags; /* formatting options */
114 int pad_char; /* padding character */
115 int pad_size; /* pad size */
116 int width; /* field width */
117 int left_prec; /* left precision */
118 int right_prec; /* right precision */
119 double value; /* just value */
120 char space_char = ' '; /* space after currency */
121
122 char cs_precedes, /* values gathered from struct lconv */
123 sep_by_space,
124 sign_posn,
125 *signstr,
126 *currency_symbol;
127
128 char *tmpptr; /* temporary vars */
129 int sverrno;
130
131 FIX_LOCALE(loc);
132
133 lc = localeconv_l(loc);
134 dst = s;
135 fmt = format;
136 asciivalue = NULL;
137 currency_symbol = NULL;
138
139 while (*fmt != 0) {
140 /* pass nonformating characters AS IS */
141 if (*fmt != '%')
142 goto literal;
143
144 /* '%' found ! */
145
146 /* "%%" mean just '%' */
147 if (*(fmt + 1) == '%') {
148 fmt++;
149 literal:
150 PRINT(*fmt++);
151 continue;
152 }
153
154 /* set up initial values */
155 flags = NEED_GROUPING | LOCALE_POSN;
156 pad_char = ' '; /* padding character is "space" */
157 pad_size = 0; /* no padding initially */
158 left_prec = -1; /* no left precision specified */
159 right_prec = -1; /* no right precision specified */
160 width = -1; /* no width specified */
161
162 /* Flags */
163 while (1) {
164 switch (*++fmt) {
165 case '=': /* fill character */
166 pad_char = *++fmt;
167 if (pad_char == '\0')
168 goto format_error;
169 continue;
170 case '^': /* not group currency */
171 flags &= ~(NEED_GROUPING);
172 continue;
173 case '+': /* use locale defined signs */
174 if ((flags & SIGN_POSN_USED) ||
175 ((lc->positive_sign[0] == '\0') &&
176 (lc->negative_sign[0] == '\0')))
177 goto format_error;
178 flags |= (SIGN_POSN_USED | LOCALE_POSN);
179 continue;
180 case '(': /* enclose negatives with () */
181 if (flags & SIGN_POSN_USED)
182 goto format_error;
183 flags |= (SIGN_POSN_USED | PARENTH_POSN);
184 continue;
185 case '!': /* suppress currency symbol */
186 flags |= SUPPRESS_CURR_SYMBOL;
187 continue;
188 case '-': /* alignment (left) */
189 flags |= LEFT_JUSTIFY;
190 continue;
191 default:
192 break;
193 }
194 break;
195 }
196
197 /* field Width */
198 if (isdigit_l((unsigned char)*fmt, loc)) {
199 GET_NUMBER(width, loc);
200 /*
201 * Do we have enough space to put number with
202 * required width ?
203 */
204 if ((unsigned int)width >= maxsize - (dst - s))
205 goto e2big_error;
206 }
207
208 /* Left precision */
209 if (*fmt == '#') {
210 if (!isdigit_l((unsigned char)*++fmt, loc))
211 goto format_error;
212 GET_NUMBER(left_prec, loc);
213 if ((unsigned int)left_prec >= maxsize - (dst - s))
214 goto e2big_error;
215 }
216
217 /* Right precision */
218 if (*fmt == '.') {
219 if (!isdigit_l((unsigned char)*++fmt, loc))
220 goto format_error;
221 GET_NUMBER(right_prec, loc);
222 if ((unsigned int)right_prec >=
223 maxsize - (dst - s) - left_prec)
224 goto e2big_error;
225 }
226
227 /* Conversion Characters */
228 switch (*fmt++) {
229 case 'i': /* use international currency format */
230 flags |= USE_INTL_CURRENCY;
231 break;
232 case 'n': /* use national currency format */
233 flags &= ~(USE_INTL_CURRENCY);
234 break;
235 default: /*
236 * required character is missing or
237 * premature EOS
238 */
239 goto format_error;
240 }
241
242 if (currency_symbol != NULL)
243 free(currency_symbol);
244 if (flags & USE_INTL_CURRENCY) {
245 currency_symbol = strdup(lc->int_curr_symbol);
246 if (currency_symbol != NULL &&
247 strlen(currency_symbol) > 3) {
248 space_char = currency_symbol[3];
249 currency_symbol[3] = '\0';
250 }
251 } else
252 currency_symbol = strdup(lc->currency_symbol);
253
254 if (currency_symbol == NULL)
255 goto end_error; /* ENOMEM. */
256
257 /* value itself */
258 value = va_arg(ap, double);
259
260 /* detect sign */
261 if (value < 0) {
262 flags |= IS_NEGATIVE;
263 value = -value;
264 }
265
266 /* fill left_prec with amount of padding chars */
267 if (left_prec >= 0) {
268 pad_size = __calc_left_pad((flags ^ IS_NEGATIVE),
269 currency_symbol, lc) -
270 __calc_left_pad(flags, currency_symbol, lc);
271 if (pad_size < 0)
272 pad_size = 0;
273 }
274
275 if (asciivalue != NULL)
276 free(asciivalue);
277 asciivalue = __format_grouped_double(value, &flags, left_prec,
278 right_prec, pad_char, lc, loc);
279 if (asciivalue == NULL)
280 goto end_error; /*
281 * errno already set to ENOMEM by
282 * malloc()
283 */
284
285 /* set some variables for later use */
286 __setup_vars(flags, &cs_precedes, &sep_by_space, &sign_posn,
287 &signstr, lc);
288
289 /*
290 * Description of some LC_MONETARY's values:
291 *
292 * p_cs_precedes & n_cs_precedes
293 *
294 * = 1 - $currency_symbol precedes the value
295 * for a monetary quantity with a non-negative value
296 * = 0 - symbol succeeds the value
297 *
298 * p_sep_by_space & n_sep_by_space
299 *
300 * = 0 - no space separates $currency_symbol
301 * from the value for a monetary quantity with a
302 * non-negative value
303 * = 1 - space separates the symbol from the value
304 * = 2 - space separates the symbol and the sign string,
305 * if adjacent; otherwise, a space separates
306 * the sign string from the value
307 *
308 * p_sign_posn & n_sign_posn
309 *
310 * = 0 - parentheses enclose the quantity and the
311 * $currency_symbol
312 * = 1 - the sign string precedes the quantity and the
313 * $currency_symbol
314 * = 2 - the sign string succeeds the quantity and the
315 * $currency_symbol
316 * = 3 - the sign string precedes the $currency_symbol
317 * = 4 - the sign string succeeds the $currency_symbol
318 */
319
320 tmpptr = dst;
321
322 while (pad_size-- > 0)
323 PRINT(' ');
324
325 if (sign_posn == 0 && (flags & IS_NEGATIVE))
326 PRINT('(');
327
328 if (cs_precedes == 1) {
329 if (sign_posn == 1 || sign_posn == 3) {
330 PRINTS(signstr);
331 if (sep_by_space == 2)
332 PRINT(' ');
333 }
334
335 if (!(flags & SUPPRESS_CURR_SYMBOL)) {
336 PRINTS(currency_symbol);
337
338 if (sign_posn == 4) {
339 if (sep_by_space == 2)
340 PRINT(space_char);
341 PRINTS(signstr);
342 if (sep_by_space == 1)
343 PRINT(' ');
344 } else if (sep_by_space == 1)
345 PRINT(space_char);
346 }
347 } else if (sign_posn == 1) {
348 PRINTS(signstr);
349 if (sep_by_space == 2)
350 PRINT(' ');
351 }
352
353 PRINTS(asciivalue);
354
355 if (cs_precedes == 0) {
356 if (sign_posn == 3) {
357 if (sep_by_space == 1)
358 PRINT(' ');
359 PRINTS(signstr);
360 }
361
362 if (!(flags & SUPPRESS_CURR_SYMBOL)) {
363 if ((sign_posn == 3 && sep_by_space == 2) ||
364 (sep_by_space == 1 &&
365 (sign_posn == 0 || sign_posn == 1 ||
366 sign_posn == 2 || sign_posn == 4)))
367 PRINT(space_char);
368 PRINTS(currency_symbol);
369 if (sign_posn == 4) {
370 if (sep_by_space == 2)
371 PRINT(' ');
372 PRINTS(signstr);
373 }
374 }
375 }
376
377 if (sign_posn == 2) {
378 if (sep_by_space == 2)
379 PRINT(' ');
380 PRINTS(signstr);
381 }
382
383 if (sign_posn == 0) {
384 if (flags & IS_NEGATIVE)
385 PRINT(')');
386 else if (left_prec >= 0)
387 PRINT(' ');
388 }
389
390 if (dst - tmpptr < width) {
391 if (flags & LEFT_JUSTIFY) {
392 while (dst - tmpptr < width)
393 PRINT(' ');
394 } else {
395 pad_size = dst - tmpptr;
396 memmove(tmpptr + width - pad_size, tmpptr,
397 pad_size);
398 memset(tmpptr, ' ', width - pad_size);
399 dst += width - pad_size;
400 }
401 }
402 }
403
404 PRINT('\0');
405 free(asciivalue);
406 free(currency_symbol);
407 return (dst - s - 1); /* size of put data except trailing '\0' */
408
409 e2big_error:
410 errno = E2BIG;
411 goto end_error;
412
413 format_error:
414 errno = EINVAL;
415
416 end_error:
417 sverrno = errno;
418 if (asciivalue != NULL)
419 free(asciivalue);
420 if (currency_symbol != NULL)
421 free(currency_symbol);
422 errno = sverrno;
423 return (-1);
424 }
425
426 static void
__setup_vars(int flags,char * cs_precedes,char * sep_by_space,char * sign_posn,char ** signstr,struct lconv * lc)427 __setup_vars(int flags, char *cs_precedes, char *sep_by_space, char *sign_posn,
428 char **signstr, struct lconv *lc)
429 {
430 if ((flags & IS_NEGATIVE) && (flags & USE_INTL_CURRENCY)) {
431 *cs_precedes = lc->int_n_cs_precedes;
432 *sep_by_space = lc->int_n_sep_by_space;
433 *sign_posn = (flags & PARENTH_POSN) ? 0 : lc->int_n_sign_posn;
434 *signstr = (lc->negative_sign[0] == '\0') ? "-" :
435 lc->negative_sign;
436 } else if (flags & USE_INTL_CURRENCY) {
437 *cs_precedes = lc->int_p_cs_precedes;
438 *sep_by_space = lc->int_p_sep_by_space;
439 *sign_posn = (flags & PARENTH_POSN) ? 0 : lc->int_p_sign_posn;
440 *signstr = lc->positive_sign;
441 } else if (flags & IS_NEGATIVE) {
442 *cs_precedes = lc->n_cs_precedes;
443 *sep_by_space = lc->n_sep_by_space;
444 *sign_posn = (flags & PARENTH_POSN) ? 0 : lc->n_sign_posn;
445 *signstr = (lc->negative_sign[0] == '\0') ? "-" :
446 lc->negative_sign;
447 } else {
448 *cs_precedes = lc->p_cs_precedes;
449 *sep_by_space = lc->p_sep_by_space;
450 *sign_posn = (flags & PARENTH_POSN) ? 0 : lc->p_sign_posn;
451 *signstr = lc->positive_sign;
452 }
453
454 /* Set default values for unspecified information. */
455 if (*cs_precedes != 0)
456 *cs_precedes = 1;
457 if (*sep_by_space == CHAR_MAX)
458 *sep_by_space = 0;
459 if (*sign_posn == CHAR_MAX)
460 *sign_posn = 1;
461 }
462
463 static int
__calc_left_pad(int flags,char * cur_symb,struct lconv * lc)464 __calc_left_pad(int flags, char *cur_symb, struct lconv *lc)
465 {
466 char cs_precedes, sep_by_space, sign_posn, *signstr;
467 int left_chars = 0;
468
469 __setup_vars(flags, &cs_precedes, &sep_by_space, &sign_posn, &signstr,
470 lc);
471
472 if (cs_precedes != 0) {
473 left_chars += strlen(cur_symb);
474 if (sep_by_space != 0)
475 left_chars++;
476 }
477
478 switch (sign_posn) {
479 case 0:
480 if (flags & IS_NEGATIVE)
481 left_chars++;
482 break;
483 case 1:
484 left_chars += strlen(signstr);
485 break;
486 case 3:
487 case 4:
488 if (cs_precedes != 0)
489 left_chars += strlen(signstr);
490 }
491 return (left_chars);
492 }
493
494 static int
get_groups(int size,char * grouping)495 get_groups(int size, char *grouping)
496 {
497 int chars = 0;
498
499 if (*grouping == CHAR_MAX || *grouping <= 0) /* no grouping ? */
500 return (0);
501
502 while (size > (int)*grouping) {
503 chars++;
504 size -= (int)*grouping++;
505 /* no more grouping ? */
506 if (*grouping == CHAR_MAX)
507 break;
508 /* rest grouping with same value ? */
509 if (*grouping == 0) {
510 chars += (size - 1) / *(grouping - 1);
511 break;
512 }
513 }
514 return (chars);
515 }
516
517 /* convert double to locale-encoded string */
518 static char *
__format_grouped_double(double value,int * flags,int left_prec,int right_prec,int pad_char,struct lconv * lc,locale_t loc)519 __format_grouped_double(double value, int *flags, int left_prec, int right_prec,
520 int pad_char, struct lconv *lc, locale_t loc)
521 {
522
523 char *rslt;
524 char *avalue;
525 int avalue_size;
526
527 size_t bufsize;
528 char *bufend;
529
530 int padded;
531
532 char *grouping;
533 const char *decimal_point;
534 const char *thousands_sep;
535 size_t decimal_point_size;
536 size_t thousands_sep_size;
537
538 int groups = 0;
539
540 grouping = lc->mon_grouping;
541 decimal_point = lc->mon_decimal_point;
542 if (*decimal_point == '\0')
543 decimal_point = lc->decimal_point;
544 thousands_sep = lc->mon_thousands_sep;
545 if (*thousands_sep == '\0')
546 thousands_sep = lc->thousands_sep;
547
548 decimal_point_size = strlen(decimal_point);
549 thousands_sep_size = strlen(thousands_sep);
550
551 /* fill left_prec with default value */
552 if (left_prec == -1)
553 left_prec = 0;
554
555 /* fill right_prec with default value */
556 if (right_prec == -1) {
557 if (*flags & USE_INTL_CURRENCY)
558 right_prec = lc->int_frac_digits;
559 else
560 right_prec = lc->frac_digits;
561
562 if (right_prec == CHAR_MAX) /* POSIX locale ? */
563 right_prec = 2;
564 }
565
566 if (*flags & NEED_GROUPING)
567 left_prec += get_groups(left_prec, grouping);
568
569 /* convert to string */
570 avalue_size = asprintf_l(&avalue, loc, "%*.*f",
571 left_prec + right_prec + 1, right_prec, value);
572 if (avalue_size < 0)
573 return (NULL);
574
575 /* make sure that we've enough space for result string */
576 bufsize = avalue_size * (1 + thousands_sep_size) + decimal_point_size +
577 1;
578 rslt = calloc(1, bufsize);
579 if (rslt == NULL) {
580 free(avalue);
581 return (NULL);
582 }
583 bufend = rslt + bufsize - 1; /* reserve space for trailing '\0' */
584
585 /* skip spaces at beginning */
586 padded = 0;
587 while (avalue[padded] == ' ') {
588 padded++;
589 avalue_size--;
590 }
591
592 if (right_prec > 0) {
593 bufend -= right_prec;
594 memcpy(bufend, avalue + avalue_size + padded - right_prec,
595 right_prec);
596 bufend -= decimal_point_size;
597 memcpy(bufend, decimal_point, decimal_point_size);
598 avalue_size -= (right_prec + 1);
599 }
600
601 if ((*flags & NEED_GROUPING) && thousands_sep_size > 0 &&
602 *grouping != CHAR_MAX && *grouping > 0) {
603 while (avalue_size > (int)*grouping) {
604 GRPCPY(*grouping);
605 GRPSEP;
606 grouping++;
607
608 /* no more grouping ? */
609 if (*grouping == CHAR_MAX)
610 break;
611
612 /* rest grouping with same value ? */
613 if (*grouping == 0) {
614 grouping--;
615 while (avalue_size > *grouping) {
616 GRPCPY(*grouping);
617 GRPSEP;
618 }
619 }
620 }
621 if (avalue_size != 0)
622 GRPCPY(avalue_size);
623 padded -= groups;
624 } else {
625 bufend -= avalue_size;
626 memcpy(bufend, avalue + padded, avalue_size);
627 /* decrease assumed $decimal_point */
628 if (right_prec == 0)
629 padded -= decimal_point_size;
630 }
631
632 /* do padding with pad_char */
633 if (padded > 0) {
634 bufend -= padded;
635 memset(bufend, pad_char, padded);
636 }
637
638 bufsize = rslt + bufsize - bufend;
639 memmove(rslt, bufend, bufsize);
640 free(avalue);
641 return (rslt);
642 }
643
644 ssize_t
strfmon(char * restrict s,size_t maxsize,const char * restrict format,...)645 strfmon(char *restrict s, size_t maxsize, const char *restrict format, ...)
646 {
647 ssize_t ret;
648 va_list ap;
649
650 va_start(ap, format);
651 ret = vstrfmon_l(s, maxsize, __get_locale(), format, ap);
652 va_end(ap);
653
654 return (ret);
655 }
656
657 ssize_t
strfmon_l(char * restrict s,size_t maxsize,locale_t loc,const char * restrict format,...)658 strfmon_l(char *restrict s, size_t maxsize, locale_t loc,
659 const char *restrict format, ...)
660 {
661 ssize_t ret;
662 va_list ap;
663
664 va_start(ap, format);
665 ret = vstrfmon_l(s, maxsize, loc, format, ap);
666 va_end(ap);
667
668 return (ret);
669 }
670