1 /// Utility functions for handling strings
2 module mecca.lib..string;
3 
4 // Licensed under the Boost license. Full copyright information in the AUTHORS file
5 
6 import std.ascii;
7 import std.conv;
8 import std..string;
9 import std.traits;
10 import std.typetuple;
11 
12 import mecca.lib.exception;
13 import mecca.lib.typedid;
14 import mecca.log;
15 
16 /**
17  * Convert a D string to a C style null terminated pointer
18  *
19  * The function uses a static buffer in order to keep the string whie needed. The function's return type is a custom type that tracks the
20  * life time of the pointer. This type is implicitly convertible to `char*` for ease of use.
21  *
22  * Most cases can use the return type as if it is a pointer to char:
23  *
24  * `fcntl.open(toStringzNGC(pathname), flags);`
25  *
26  * If keeping the pointer around longer is needed, it should be stored in a variable of type `auto`:
27  *
28  * `auto fileNameC = toStringzNGC(fileName);`
29  *
30  * This can be kept around until end of scope. It can be released earlier with the `release` function:
31  *
32  * `fileNameC.release();`
33  *
34  * Params:
35  *   dString = the D string to be converted to zero terminated format.
36  *
37  * Returns:
38  *   A custom type with a destructor, a function called `release()`, and an implicit conversion to `char*`.
39  *
40  */
41 auto toStringzNGC(string dString) nothrow @trusted @nogc {
42     enum MaxStringSize = 4096;
43     __gshared static char[MaxStringSize] buffer;
44     __gshared static bool inUse;
45 
46     ASSERT!"toStringzNGC called while another instance is still in use. Buffer currently contains: %s"(!inUse, buffer[]);
47     // DMDBUG? The following line triggers a closure GC
48     //ASSERT!"toStringzNGC got %s chars long string, maximal string size is %s"(dString.length<MaxStringSize, dString.length, MaxStringSize-1);
49 
50     char[] cString = buffer[0..dString.length + 1];
51     cString[0..dString.length] = dString[];
52     cString[dString.length] = '\0';
53 
54     static struct toStringzNGCContext {
55     private:
56         alias LengthType = ushort;
57         static assert((1 << (LengthType.sizeof * 8)) > MaxStringSize, "Length type not big enough for buffer");
58         LengthType length;
59 
60     public:
61         @disable this(this);
62 
63         this(LengthType length) nothrow @trusted @nogc {
64             this.length = length;
65             inUse = true;
66         }
67 
68         ~this() nothrow @safe @nogc {
69             release();
70         }
71 
72         void release() nothrow @trusted @nogc {
73             enum UnusedString = "toStringzNGC result used after already stale\0";
74             inUse = false;
75             buffer[0..UnusedString.length] = UnusedString[];
76         }
77 
78         @property char* ptr() nothrow @trusted @nogc {
79             return &buffer[0];
80         }
81 
82         alias ptr this;
83     }
84 
85     return toStringzNGCContext(cast(toStringzNGCContext.LengthType)cString.length);
86 }
87 
88 
89 struct ToStringz(size_t N) {
90     char[N] buffer;
91 
92     @disable this();
93     @disable this(this);
94 
95     this(const(char)[] str) nothrow @safe @nogc {
96         opAssign(str);
97     }
98     ref ToStringz opAssign(const(char)[] str) nothrow @safe @nogc {
99         assert (str.length < buffer.length, "Input string too long");
100         buffer[0 .. str.length] = str;
101         buffer[str.length] = '\0';
102         return this;
103     }
104     @property const(char)* ptr() const nothrow @system @nogc {
105         return buffer.ptr;
106     }
107     alias ptr this;
108 }
109 
110 unittest {
111     import core.stdc..string: strlen;
112 
113     assert (strlen(ToStringz!64("hello")) == 5);
114     assert (strlen(ToStringz!64("kaki")) == 4);
115     {
116         auto s = ToStringz!64("0123456789");
117         assert (strlen(s) == 10);
118         s = "moshe";
119         assert (strlen(s) == 5);
120     }
121     assert (strlen(ToStringz!64("mishmish")) == 8);
122 }
123 
124 
125 private enum FMT: ubyte {
126     STR,
127     CHR,
128     DEC,
129     HEX,
130     PTR,
131     FLT,
132 }
133 
134 @notrace
135 ulong getNextNonDigitFrom(string fmt){
136     ulong idx;
137     foreach(c; fmt){
138         if ("0123456789+-.".indexOf(c) < 0) {
139             return idx;
140         }
141         ++idx;
142     }
143     return idx;
144 }
145 
146 template splitFmt(string fmt) {
147     template pair(int j, FMT f) {
148         enum size_t pair = (j << 8) | (cast(ubyte)f);
149     }
150 
151     template helper(int from, int j) {
152         enum idx = fmt[from .. $].indexOf('%');
153         static if (idx < 0) {
154             enum helper = TypeTuple!(fmt[from .. $]);
155         }
156         else {
157             enum idx1 = idx + from;
158             static if (idx1 >= fmt.length - 1) {
159                 static assert (false, "Expected formatter after %");
160             }else{
161                 enum idx2 = idx1 + getNextNonDigitFrom(fmt [idx1+1 .. $]);
162                 //pragma(msg, fmt);
163                 //pragma(msg, idx2);
164                 static if (fmt[idx2+1] == 's') {
165                     enum helper = TypeTuple!(fmt[from .. idx2], pair!(j, FMT.STR), helper!(idx2+2, j+1));
166                 }
167                 else static if (fmt[idx2+1] == 'c') {
168                     enum helper = TypeTuple!(fmt[from .. idx2], pair!(j, FMT.CHR), helper!(idx2+2, j+1));
169                 }
170                 else static if (fmt[idx2+1] == 'n') {
171                     enum helper = TypeTuple!(fmt[from .. idx2], pair!(j, FMT.STR), helper!(idx2+2, j+1));
172                 }
173                 else static if (fmt[idx2+1] == 'd') {
174                     enum helper = TypeTuple!(fmt[from .. idx2], pair!(j, FMT.DEC), helper!(idx2+2, j+1));
175                 }
176                 else static if (fmt[idx2+1] == 'x') {
177                     enum helper = TypeTuple!(fmt[from .. idx2], pair!(j, FMT.HEX), helper!(idx2+2, j+1));
178                 }
179                 else static if (fmt[idx2+1] == 'b') {  // should be binary, but use hex for now
180                     enum helper = TypeTuple!(fmt[from .. idx2], pair!(j, FMT.HEX), helper!(idx2+2, j+1));
181                 }
182                 else static if (fmt[idx2+1] == 'p') {
183                     enum helper = TypeTuple!(fmt[from .. idx2], pair!(j, FMT.PTR), helper!(idx2+2, j+1));
184                 }
185                 else static if (fmt[idx2+1] == 'f' || fmt[idx2+1] == 'g') {
186                     enum helper = TypeTuple!(fmt[from .. idx2], pair!(j, FMT.FLT), helper!(idx2+2, j+1));
187                 }
188                 else static if (fmt[idx2+1] == '%') {
189                     enum helper = TypeTuple!(fmt[from .. idx2+1], helper!(idx2+2, j));
190                 }
191                 else {
192                     static assert (false, "Invalid formatter '"~fmt[idx2+1]~"'");
193                 }
194             }
195         }
196     }
197 
198     template countFormatters(tup...) {
199         static if (tup.length == 0) {
200             enum countFormatters = 0;
201         }
202         else static if (is(typeof(tup[0]) == size_t)) {
203             enum countFormatters = 1 + countFormatters!(tup[1 .. $]);
204         }
205         else {
206             enum countFormatters = countFormatters!(tup[1 .. $]);
207         }
208     }
209 
210     alias tokens = helper!(0, 0);
211     alias numFormatters = countFormatters!tokens;
212 }
213 
214 @notrace @nogc char[] formatDecimal(size_t W = 0, char fillChar = ' ', T)(char[] buf, T val) pure nothrow if (is(typeof({ulong v = val;}))) {
215     const neg = (isSigned!T) && (val < 0);
216     size_t len = neg ? 1 : 0;
217     ulong v = neg ? -long(val) : val;
218 
219     auto tmp = v;
220     while (tmp) {
221         tmp /= 10;
222         len++;
223     }
224     static if (W > 0) {
225         if (W > len) {
226             buf[0 .. W - len] = fillChar;
227             len = W;
228         }
229     }
230 
231     if (v == 0) {
232         static if (W > 0) {
233             buf[len-1] = '0';
234         }
235         else {
236             buf[len++] = '0';
237         }
238     }
239     else {
240         auto idx = len;
241         while (v) {
242             buf[--idx] = "0123456789"[v % 10];
243             v /= 10;
244         }
245         if (neg) {
246             buf[--idx] = '-';
247         }
248     }
249     return buf[0 .. len];
250 }
251 
252 @notrace @nogc char[] formatDecimal(char[] buf, bool val) pure nothrow {
253     if (val) {
254         return cast(char[])"1";
255     }
256     return cast(char[])"0";
257 }
258 
259 unittest {
260     char[100] buf;
261     assert (formatDecimal!10(buf, -1234) == "     -1234");
262     assert (formatDecimal!10(buf, 0)     == "         0");
263     assert (formatDecimal(buf, -1234)    == "-1234");
264     assert (formatDecimal(buf, 0)        == "0");
265     assert (formatDecimal!3(buf, 1234)   == "1234");
266     assert (formatDecimal!3(buf, -1234)  == "-1234");
267     assert (formatDecimal!3(buf, 0)      == "  0");
268     assert (formatDecimal!(3,'0')(buf, 0)      == "000");
269     assert (formatDecimal!(3,'a')(buf, 0)      == "aa0");
270     assert (formatDecimal!(10, '0')(buf, -1234) == "00000-1234");
271 }
272 
273 @notrace @nogc char[] formatHex(size_t W=0)(char[] buf, ulong val) pure nothrow {
274     size_t len = 0;
275     auto v = val;
276 
277     while (v) {
278         v >>= 4;
279         len++;
280     }
281     static if (W > 0) {
282         if (W > len) {
283             buf[0 .. W - len] = '0';
284             len = W;
285         }
286     }
287 
288     v = val;
289     if (v == 0) {
290         static if (W == 0) {
291             buf[0] = '0';
292             len = 1;
293         }
294     }
295     else {
296         auto idx = len;
297         while (v) {
298             buf[--idx] = "0123456789ABCDEF"[v & 0x0f];
299             v >>= 4;
300         }
301     }
302     return buf[0 .. len];
303 }
304 
305 unittest {
306     import mecca.lib.exception;
307     char[100] buf;
308     assertEQ(formatHex(buf, 0x123), "123");
309     assertEQ(formatHex!10(buf, 0x123), "0000000123");
310     assertEQ(formatHex(buf, 0), "0");
311     assertEQ(formatHex!10(buf, 0), "0000000000");
312     assertEQ(formatHex!10(buf, 0x123456789), "0123456789");
313     assertEQ(formatHex!10(buf, 0x1234567890), "1234567890");
314     assertEQ(formatHex!10(buf, 0x1234567890a), "1234567890A");
315 }
316 
317 @notrace @nogc char[] formatPtr(char[] buf, ulong p) pure nothrow {
318     return formatPtr(buf, cast(void*)p);
319 }
320 
321 @notrace @nogc char[] formatPtr(char[] buf, const void* p) pure nothrow {
322     if (p is null) {
323         buf[0 .. 4] = "null";
324         return buf[0 .. 4];
325     }
326     else {
327         import std.stdint : intptr_t;
328         return formatHex!((void*).sizeof*2)(buf, cast(intptr_t)p);
329     }
330 }
331 
332 @notrace @nogc char[] formatFloat(char[] buf, double val) pure nothrow {
333     assert (false, "Not implemented");
334 }
335 
336 @notrace @nogc string enumToStr(E)(E value) pure nothrow {
337     switch (value) {
338         foreach(name; __traits(allMembers, E)) {
339             case __traits(getMember, E, name):
340                 return name;
341         }
342         default:
343             return null;
344     }
345 }
346 
347 unittest {
348     import mecca.lib.exception;
349     import std..string: format, toUpper;
350     char[100] buf;
351     int p;
352 
353     assertEQ(formatPtr(buf, 0x123), "0000000000000123");
354     assertEQ(formatPtr(buf, 0), "null");
355     assertEQ(formatPtr(buf, null), "null");
356     assertEQ(formatPtr(buf, &p), format("%016x", &p).toUpper);
357 }
358 
359 @notrace @nogc string nogcFormat(string fmt, T...)(char[] buf, T args) pure nothrow {
360     alias sfmt = splitFmt!fmt;
361     static assert (sfmt.numFormatters == T.length, "Expected " ~ text(sfmt.numFormatters) ~
362         " arguments, got " ~ text(T.length));
363 
364     char[] p = buf;
365     @nogc pure nothrow
366     void advance(const(char[]) str) {
367         p = p[str.length..$];
368     }
369     @nogc pure nothrow
370     void write(const(char[]) str) {
371         p[0..str.length] = str;
372         advance(str);
373     }
374     foreach(tok; sfmt.tokens) {
375         static if (is(typeof(tok) == string)) {
376             static if (tok.length > 0) {
377                 write(tok);
378             }
379         }
380         else static if (is(typeof(tok) == size_t)) {
381             enum j = tok >> 8;
382             enum f = cast(FMT)(tok & 0xff);
383 
384             alias Typ = T[j];
385             auto val = args[j];
386 
387             static if (f == FMT.STR) {
388                 static if (is(typeof(advance(val.nogcToString(p))))) {
389                     advance(val.nogcToString(p));
390                 } else static if (is(Typ == string) || is(Typ == char[]) || is(Typ == char[Len], uint Len)) {
391                     write(val[]);
392                 } else static if (is(Typ == enum)) {
393                     auto tmp = enumToStr(val);
394                     if (tmp is null) {
395                         advance(p.nogcFormat!"%s(%d)"(Typ.stringof, val));
396                     } else {
397                         write(tmp);
398                     }
399                 } else static if (is(Typ == U[N], U, size_t N) || is(Typ == U[], U)) {
400                     write("[");
401                     foreach(i, x; val) {
402                         if(i > 0) {
403                             advance(p.nogcFormat!", %s"(x));
404                         } else {
405                             advance(p.nogcFormat!"%s"(x));
406                         }
407                     }
408                     write("]");
409                 } else static if (isTypedIdentifier!Typ) {
410                     advance(p.nogcFormat!(Typ.name ~ "<%s>")(val.value));
411                 } else static if (isPointer!Typ) {
412                     advance(formatPtr(p, val));
413                 } else static if (is(Typ : ulong)) {
414                     advance(formatDecimal(p, val));
415                 } else static if (is(Typ == struct)) {
416                     {
417                         enum Prefix = Typ.stringof ~ "(";
418                         write(Prefix);
419                     }
420                     alias Names = FieldNameTuple!Typ;
421                     foreach(i, field; val.tupleof) {
422                         enum string Name = Names[i];
423                         enum Prefix = (i == 0 ? "" : ", ") ~ Name ~ " = ";
424                         write(Prefix);
425                         // TODO: Extract entire FMT.STR hangling to nogcToString and use that:
426                         advance(p.nogcFormat!"%s"(field));
427                     }
428                     write(")");
429                 } else {
430                     static assert (false, "Expected string, enum or integer, not " ~ Typ.stringof);
431                 }
432             }
433             else static if (f == FMT.CHR) {
434                 static assert (is(T[j] : char));
435                 write((&val)[0..1]);
436             }
437             else static if (f == FMT.DEC) {
438                 static assert (is(T[j] : ulong));
439                 advance(formatDecimal(p, val));
440             }
441             else static if (f == FMT.HEX) {
442                 static assert (is(T[j] : ulong));
443                 write("0x");
444                 advance(formatHex(p, val));
445             }
446             else static if (f == FMT.PTR) {
447                 static assert (is(T[j] : ulong) || isPointer!(T[j]));
448                 advance(formatPtr(p, val));
449             }
450             else static if (f == FMT.FLT) {
451                 static assert (is(T[j] : double));
452                 advance(formatFloat(p, val));
453             }
454         }
455         else {
456             static assert (false);
457         }
458     }
459 
460     auto len = p.ptr - buf.ptr;
461     import std.exception : assumeUnique;
462     return buf[0 .. len].assumeUnique;
463 }
464 
465 @notrace @nogc string nogcFormatTmp(string fmt, T...)(T args) nothrow {
466     // the lengths i have to go to fool `pure`
467     static __gshared char[1024] tmpBuf;
468 
469     return nogcFormat!fmt(cast(char[])tmpBuf, args);
470 }
471 
472 unittest {
473     char[100] buf;
474     assert (nogcFormat!"hello %s %s %% world %d %x %p"(buf, [1, 2, 3], "moshe", -567, 7, 7) == "hello [1, 2, 3] moshe % world -567 0x7 0000000000000007");
475 }
476 
477 unittest {
478     import std.exception;
479     import core.exception : RangeError;
480 
481     auto fmt(string fmtStr, size_t size = 16, Args...)(Args args) {
482         auto buf = new char[size];
483         return nogcFormat!fmtStr(buf, args);
484     }
485 
486     static assert(fmt!"abcd abcd" == "abcd abcd");
487     static assert(fmt!"123456789a" == "123456789a");
488     version (D_NoBoundsChecks) {} else {
489         assertThrown!RangeError(fmt!("123412341234", 10));
490     }
491 
492     // literal escape
493     static assert(fmt!"123 %%" == "123 %");
494     static assert(fmt!"%%%%" == "%%");
495 
496     // %d
497     static assert(fmt!"%d"(1234) == "1234");
498     static assert(fmt!"ab%dcd"(1234) == "ab1234cd");
499     static assert(fmt!"ab%d%d"(1234, 56) == "ab123456");
500 
501     // %x
502     static assert(fmt!"%x"(0x1234) == "0x1234");
503 
504     // %p
505     static assert(fmt!("%p", 20)(0x1234) == "0000000000001234");
506 
507     // %s
508     static assert(fmt!"12345%s"("12345") == "1234512345");
509     static assert(fmt!"12345%s"(12345) == "1234512345");
510     enum Floop {XXX, YYY, ZZZ}
511     static assert(fmt!"12345%s"(Floop.YYY) == "12345YYY");
512 
513     // Arg num
514     static assert(!__traits(compiles, fmt!"abc"(5)));
515     static assert(!__traits(compiles, fmt!"%d"()));
516     static assert(!__traits(compiles, fmt!"%d a %d"(5)));
517 
518     // Format error
519     static assert(!__traits(compiles, fmt!"%"()));
520     static assert(!__traits(compiles, fmt!"abcd%d %"(15)));
521     static assert(!__traits(compiles, fmt!"%$"(1)));
522     //static assert(!__traits(compiles, fmt!"%s"(1)));
523     static assert(!__traits(compiles, fmt!"%d"("hello")));
524     static assert(!__traits(compiles, fmt!"%x"("hello")));
525 
526     static assert(fmt!"Hello %s"(5) == "Hello 5");
527     alias Moishe = TypedIdentifier!("Moishe", ushort);
528     static assert(fmt!"Hello %s"(Moishe(5)) == "Hello Moishe<5>");
529 
530     struct Foo { int x, y; }
531     static assert(fmt!("Hello %s", 40)(Foo(1, 2)) == "Hello Foo(x = 1, y = 2)");
532 }
533 
534 @notrace @nogc nothrow pure
535 string nogcRtFormat(T...)(char[] buf, string fmt, T args) {
536     size_t fmtIdx = 0;
537     size_t bufIdx = 0;
538 
539     @notrace @nogc nothrow pure
540     char nextFormatter() {
541         while (true) {
542             long pctIdx = -1;
543             foreach(j, ch; fmt[fmtIdx .. $]) {
544                 if (ch == '%') {
545                     pctIdx = fmtIdx + j;
546                     break;
547                 }
548             }
549             if (pctIdx < 0) {
550                 return 's';
551             }
552 
553             auto fmtChar = (pctIdx < fmt.length - 1) ? fmt[pctIdx + 1] : 's';
554 
555             auto tmp = fmt[fmtIdx .. pctIdx];
556             buf[bufIdx .. bufIdx + tmp.length] = tmp;
557             bufIdx += tmp.length;
558             fmtIdx = pctIdx + 2;
559 
560             if (fmtChar == '%') {
561                 buf[bufIdx++] = '%';
562                 continue;
563             }
564             return fmtChar;
565         }
566     }
567 
568     foreach(i, U; T) {
569         auto fmtChar = nextFormatter();
570 
571         static if (is(U == string) || is(U: char[])) {
572             assert (fmtChar == 's'/*, text(fmtChar)*/);
573             buf[bufIdx .. bufIdx + args[i].length] = args[i];
574             bufIdx += args[i].length;
575         }
576         else static if (is(U == char*) || is(U == const(char)*) || is (U == const(char*))) {
577             if (fmtChar == 's') {
578                 auto tmp = fromStringz(args[i]);
579                 buf[bufIdx .. bufIdx + tmp.length] = tmp;
580                 bufIdx += tmp.length;
581             }
582             else if (fmtChar == 'p') {
583                 bufIdx += formatPtr(buf[bufIdx .. $], args[i]).length;
584             }
585             else {
586                 assert (false /*, text(fmtChar)*/);
587             }
588         }
589         else static if (is(U == enum)) {
590             if (fmtChar == 's') {
591                 auto tmp = enumToStr(args[i]);
592                 if (tmp is null) {
593                     bufIdx += nogcFormat!"%s(%d)"(buf[bufIdx .. $], U.stringof, args[i]).length;
594                 }
595                 else {
596                     buf[bufIdx .. bufIdx + tmp.length] = tmp;
597                     bufIdx += tmp.length;
598                 }
599             }
600             else if (fmtChar == 'x') {
601                 buf[bufIdx .. bufIdx + 2] = "0x";
602                 bufIdx += 2;
603                 bufIdx += formatHex(buf[bufIdx .. $], args[i]).length;
604             }
605             else if (fmtChar == 'p') {
606                 bufIdx += formatPtr(buf[bufIdx .. $], args[i]).length;
607             }
608             else if (fmtChar == 's' || fmtChar == 'd') {
609                 bufIdx += formatDecimal(buf[bufIdx .. $], args[i]).length;
610             }
611             else {
612                 assert (false /*, text(fmtChar)*/);
613             }
614         }
615         else static if (is(U : ulong)) {
616             if (fmtChar == 'x') {
617                 buf[bufIdx .. bufIdx + 2] = "0x";
618                 bufIdx += 2;
619                 bufIdx += formatHex(buf[bufIdx .. $], args[i]).length;
620             }
621             else if (fmtChar == 'p') {
622                 bufIdx += formatPtr(buf[bufIdx .. $], args[i]).length;
623             }
624             else if (fmtChar == 's' || fmtChar == 'd') {
625                 bufIdx += formatDecimal(buf[bufIdx .. $], args[i]).length;
626             }
627             else {
628                 assert (false /*, text(fmtChar)*/);
629             }
630         }
631         else static if (is(U == TypedIdentifier!X, X...)) {
632             bufIdx += nogcFormat!"%s(%d)"(buf[bufIdx .. $], U.name, args[i].value).length;
633         }
634         else static if (isPointer!U) {
635             bufIdx += formatPtr(buf[bufIdx .. $], args[i]).length;
636         }
637         else {
638             static assert (false, "Cannot format " ~ U.stringof);
639         }
640     }
641 
642     // tail
643     if (fmtIdx < fmt.length) {
644         auto tmp = fmt[fmtIdx .. $];
645         buf[bufIdx .. bufIdx + tmp.length] = tmp;
646         bufIdx += tmp.length;
647     }
648 
649     return cast(string)buf[0 .. bufIdx];
650 }
651 
652 unittest {
653     import mecca.lib.exception;
654     char[100] buf;
655     assertEQ(nogcRtFormat(buf, "hello %% %s world %d", "moshe", 15), "hello % moshe world 15");
656 }
657 
658 
659 template HexFormat(T) if( isIntegral!T )
660 {
661     static if( T.sizeof == 1 )
662         enum HexFormat = "%02x";
663     else static if( T.sizeof == 2 )
664         enum HexFormat = "%04x";
665     else static if( T.sizeof == 4 )
666         enum HexFormat = "%08x";
667     else static if( T.sizeof == 8 )
668         enum HexFormat = "%016x";
669     else
670         static assert(false);
671 }
672 
673 @notrace static string hexArray(T)( const T[] array ) if( isIntegral!T ) {
674     import std..string;
675     auto res = "[";
676     foreach( i, element; array ) {
677         if( i==0 )
678             res ~= " ";
679         else
680             res ~= ", ";
681 
682         res ~= format(HexFormat!T, element);
683     }
684     res ~= " ]";
685     return res;
686 }
687 
688 
689 @notrace string buildStructFormatCode(string fmt, string structName, string conversionFunction = "text") {
690     string iter = fmt[];
691     string result = "`";
692     while (0 < iter.length) {
693         auto phStart = iter.indexOf('{');
694         auto phEnd = iter.indexOf('}');
695 
696         // End of format string
697         if (phStart < 0 && phEnd < 0) {
698             result ~= iter;
699             break;
700         }
701 
702         assert (0 <= phStart, "single '}' in `" ~ fmt ~ "`");
703         assert (0 <= phEnd, "single '{' in `" ~ fmt ~ "`");
704         assert (phStart < phEnd, "single '}' in `" ~ fmt ~ "`");
705 
706         result ~= iter[0 .. phStart];
707         result ~= "` ~ " ~ conversionFunction ~ "(" ~ structName ~ "." ~ iter[phStart + 1 .. phEnd] ~ ") ~ `";
708         iter = iter[phEnd + 1 .. $];
709     }
710     return result ~ "`";
711 }
712 
713 unittest {
714     struct Foo {
715         int x;
716         string y;
717     }
718     string formatFoo(Foo foo) {
719         return mixin(buildStructFormatCode("x is {x} and y is {y}", "foo"));
720     }
721     assert (formatFoo(Foo(1, "a")) == "x is 1 and y is a");
722     assert (formatFoo(Foo(2, "b")) == "x is 2 and y is b");
723 }
724 
725 struct StaticFormatter {
726     char[] buf;
727     size_t offset;
728 
729     this(char[] buf) @nogc nothrow @safe {
730         this.buf = buf;
731         offset = 0;
732     }
733     @notrace void rewind() nothrow @nogc {
734         offset = 0;
735     }
736     @notrace void append(char ch) @nogc {
737         buf[offset .. offset + 1] = ch;
738         offset++;
739     }
740     @notrace void append(string s) @nogc {
741         buf[offset .. offset + s.length] = s;
742         offset += s.length;
743     }
744     @notrace void append(string FMT, T...)(T args) @nogc {
745         auto s = nogcFormat!FMT(buf[offset .. $], args);
746         offset += s.length;
747     }
748     @notrace void accumulate(const(char)[] function(char[]) @nogc dg) @nogc {
749         auto s = dg(remaining);
750         assert(cast(void*)s.ptr == remaining.ptr, "dg() returned wrong buffer");
751         skip(s.length);
752     }
753     @notrace void accumulate(const(char)[] delegate(char[]) @nogc dg) @nogc {
754         auto s = dg(remaining);
755         assert(cast(void*)s.ptr == remaining.ptr, "dg() returned wrong buffer");
756         skip(s.length);
757     }
758     @notrace void accumulate(alias F, T...)(T args) @nogc {
759         auto s = F(remaining, args);
760         assert(cast(void*)s.ptr == remaining.ptr, "dg() returned wrong buffer");
761         skip(s.length);
762     }
763 
764     @property string text() @nogc {
765         return cast(string)buf[0 .. offset];
766     }
767     @property char[] remaining() @nogc {
768         return buf[offset .. $];
769     }
770     @notrace void skip(size_t count) @nogc {
771         assert (offset + count <= buf.length, "overflow");
772         offset += count;
773     }
774 }
775 
776 unittest {
777     char[100] buf;
778     auto sf = StaticFormatter(buf);
779     sf.append!"a=%s b=%s "(1, 2);
780     sf.append!"c=%s d=%s"(3, 4);
781     assert(sf.text == "a=1 b=2 c=3 d=4", sf.text);
782 }
783 
784