Source file: /~heha/hsn/esptool.zip/esptool.cpp

#include "esptool.h"
#include "info.h"
#include "image.h"
#include "elffile.h"
#include "miniz.h"
#include "espcom.h"
#include <malloc.h>	// _alloca, realloc
#include "miniz.h"


void Dumpfile::parseFrontOfName() {
 const char*s=n,*q=strchr(s,':');
 if (!q) return;
 if (q==s) return;		// At least one (digit) character before colon
// If the <fname> argument contains a ":", check whether number given is a candidate for hex address
 char*e1,*e2;
 addr=strtoul(s,&e1,0);	// hexadecimal with 0x (to avoid disambiguity with "C:\user\...")
 char c=*(e2=e1);
 bool lenAvail = (c|0x20)=='l';	// "L" or "l" available
 if (lenAvail) {
  size=strtoul(e1+1,&e2,0);// After "L" or "l", there MUST be a number! Otherwise, it's invalid.
  if (e2==e1+1) return;		// No number: invalid
  if (!size) return;		// Number is zero: invalid
 }
 if (e2!=q) return;		// Must point to the ':'
 if (lenAvail) flags|=8;
 if (e1!=s) flags|=4;
 n=q+1;			// true file name starts after ":" as prefix syntax is correct
}

uint32 Dumpfile::strlenp1(uint32 i) const{
 if (i>=size) return 0;		// don't scan, and don't add anymore
 const byte*p=(const byte*)memchr(d+i,0,size-i);
 if (p) return uint32(p-d)-i+1;	// a terminating zero found
 return size-i;			// no zero found: assume end-of-buffer
}

Dumpfile::Dumpfile(byte chip_id):n(0),d(0),size(0),flags(0) {
 const byte*img = esp::stub(chip_id);	// load from resource
 if (!img) return;
 if (!ESPFirmwareImage::valid(img)) return;	// resource error
 d=const_cast<byte*>(img);
}


Dumpfile::Dumpfile(byte*d,uint32 s,uint32 a):n(0),d(d),size(s),addr(a),flags(3) {} // von Parser

// Memory dump files must either have an address in front of the real file name,
// or the hexadecimal address as part of the file name.
// flag bits	input		output
// flags.0:	0		address given by suffix
// flags.1:	0		length given by file size
// flags.2:	0		address given by prefix (has precedence over Bit 0)
// flags.3:	0		length given by prefix (has precedence over Bit 1)
// flags.4	0		file is gzip compressed: File pointer set to raw compressed data
// File is always open for reading (so switch to verify etc.)
Dumpfile::Dumpfile(const char*na):n(na),d(0),flags(0) {
 parseFrontOfName();
 FILE*f=fopen(n,"rb");
 if (f) {
  if (!(flags&8)) {
   fseek(f,0,SEEK_END);
   size=ftell(f);
   fseek(f,0,SEEK_SET);
   flags|=2;
  }
  d=new byte[size];
  flags|=1<<7;		// data owned!
  if (fread(d,1,size,f)!=size) println(0x210,n);	// "Error reading file %s"
  fclose(f);
  if (d[0]==0x1F && d[1]==0x8B) {	// GZIP signature
   byte flg=d[3];
   uint32 i=10;				// sizeof fixed GZIP header
   if (flg&1<<2) i+=2+*(uint16*)(d+i);	// skip FEXTRA
   if (flg&1<<3) i+=strlenp1(i);	// skip FNAME
   if (flg&1<<4) i+=strlenp1(i);	// skip FCOMMENT
   if (flg&1<<1) i+=2;			// skip (don't check) CRC16
   if (i<size-8) {
    d[0]=0x78;			// make ZLIB header
    d[1]=0xDA;
    memmove(d+2,d+i,size-i-6);	// kill GZIP header
    memcpy(&ucrc,d+size-8,8);	// get uncompressed CRC and size
    size-=i+2;	// reduce compressed data size by header and footer
// TODO: Decompress, only to generate Adler-32 value, but is it really needed by bootloader's inflate?
    *reinterpret_cast<uint32*>(d+size-4)=0;
    flags|=1<<4;
   }else usize=size;
  }else usize=size;
 }
 if (!(flags&4)) {	// if not "address given", take address from file name
  const char*s=n,  
	*pf=strrchr(s,'/'),
#ifdef WIN32
	*pb=strrchr(s,'\\'),	// Windows only
	*pn=pf&&pb?pf>pb?pf+1:pb+1:pf?pf+1:pb?pb+1:s,
#else
	*pn=pf?pf+1:s,
#endif
	*pa=strrchr(pn,'@');
  if (pa) {
   char*pe;
   addr = strtoul(++pa,&pe,16);	// always hexadecimal without prefix
   if (pe!=pa && (*pe=='.' || !*pe)) flags|=1;
  }
 }
}

Dumpfile::~Dumpfile() {if (flags&1<<7) {delete[] d; flags&=~(1<<7);}}

/* Opens file <fname>
Returns start address at <sadr> when given, and dissection occurs.
There are two options for placement (i.e. first address of binary file):
1. Valid hexadecimal address in front of ':' and real path name
2. Valid hexadecimal address after last '@' character in file name
   So "bootloader@1000.bin", "partitions@8000.bin" and "main_program@10000.bin"
   will automagically scatter to the right memory areas.
   A load address given by address prefix (with ':') has precedence.
If placement info is given, <fname> is loaded as plain binary,
i.e. no segment iteration will be made.
With no placement information, one-layer iteration will be made.
This way, multiple ESP32-Image files may be placed into one cantainer-ESP32-Image!
*/
bool parseBinFile(const char*fname,cb2_t cb,void*cbd,uint32*sadr) {
 Dumpfile df(fname);
 if (!df.data()) {
  println("Could not open file %s!",fname);
  return false;
 }
 if (df.flags&8) {
  println("Length argument not allowed here");
  return false;
 }
 if (df.flags&1<<4) return cb(cbd,df);
 if (ELFFile::valid(df.data(),df.size))
	return ELFFile::parse(df,cb,cbd,sadr);	// delegate
// UNCLEAR: Allowed to ôdecodeö firmware images for flash into partitions?
 if (df.flags&5) {		// address given
  return cb(cbd,df);		// BinΣrdatei: 1 äBlobô
 }else{				// no address given
  if (ESPFirmwareImage::valid(df.data(),df.size))
	return ESPFirmwareImage::parse(df,cb,cbd,sadr);	// Sonderfall äZwiebackô
  println("Address necessary");
  return false;
 }
}

const char*param(const char*arg, const char*const*&argp) {
 switch (*arg) {
  case '=': return ++arg;
  case 0: arg=*++argp; if (!arg) Fatal(0x102); return arg;	// "Missing argument"
  default: return arg;
//  default: Fatal(0x013);		// "Command line garbage"
 }
}

// Find in string-pointer list, terminated by nullptr
int find(const char*needle, const char*const haystack[]) {
 for (int i=0;*haystack;i++,haystack++) if (!strcmp(needle,*haystack)) return i;
 return -1;
}

// Find in string list, concatenated by "\0", terminated by "\0\0"
int find(const char*needle, const char*haystack) {
 for (int i=0;*haystack;i++,haystack+=strlen(haystack)+1) if (!strcmp(needle,haystack)) return i;
 return -1;
}

void selftest() {
 Sha256 sha256;
 if (!sha256.selftest()) println("Fehler im %s-Algorithmus!","SHA-256");
 Md5 md5;
 if (!md5.selftest()) println("Fehler im %s-Algorithmus!","MD5");
 const void*  data=selftest;
 const size_t dlen=3000;
 size_t len;
 void*packed = Deflate::mem_to_heap(data,dlen,len,0);
 size_t elen;
 void*extracted = Inflate::mem_to_heap(packed,len,elen,0);
 if (elen!=dlen || memcmp(extracted,data,dlen)) println("Fehler im %s-Algorithmus!","Zip");
 free(packed);
 free(extracted);
}


int _cdecl main(int argc, char**argv) {
 enum op{
  none,
  version,
  image_info,
  make_image,
  elf2image,
  merge_bin,
  load_ram,	// first op that needs serial connection to chip
  read_mem,
  write_mem,
  dump_mem,
  erase_region,
  erase_flash,
  write_flash,
  read_flash,
  verify_flash,
  read_flash_status,
  write_flash_status,
  run,
  read_mac,
  flash_id,
  chip_id,
  get_security_info,
  test,
 }operation=none;
// Must be in sync with previous enum, beginning with first
 static const char operations[]={
  "version"		"\0"
  "image_info"		"\0"
  "make_image"		"\0"
  "elf2image"		"\0"
  "merge_bin"		"\0"
  "load_ram"		"\0"
  "read_mem"		"\0"
  "write_mem"		"\0"
  "dump_mem"		"\0"
  "erase_region"	"\0"
  "erase_flash"		"\0"
  "write_flash"		"\0"
  "read_flash"		"\0"
  "verify_flash"	"\0"
  "read_flash_status"	"\0"
  "write_flash_status"	"\0"
  "run"			"\0"
  "read_mac"		"\0"
  "flash_id"		"\0"
  "chip_id"		"\0"
  "get_security_info"	"\0"
  "test"		"\0"
 };

 static const char*const fmode[]={"qio","qout","dio","dout",0};
 static const char*const fsize[]={"1MB","2MB","4MB","8MB",0};	// Hi-Nibble 0,1,2,3
 static const char*const ffreq[]={"40M","26M","20M","80M",0};	// Lo-Nibble 0,1,2,15

 tty.init();
 selftest();

 ComSel comsel;	// initialisiert sich per Defaultkonstruktor aus Registry!

 struct Args{
  char chip;	// (82)66 oder 32
  char flash_mode,flash_size,flash_freq;
  unsigned entrypoint;
  const char*input,*output;
 }args;

 memset(&args,0,sizeof args);	// alles au▀er Comsel l÷schen

 const char*const*argp;
 for (argp=argv+1;;argp++) {
  const char*arg=*argp;
  if (!arg) break;	// Ende der Kommandozeilenparameter
  switch (arg[0]) {
   case '-': switch (arg[1]) {
    case 'p': comsel.port = (BYTE)strtoul(param(arg+2,argp)+3,0,0)-1; break;	// "COM" stillschweigend ⁿbergehen
    case 'b': comsel.baud = strtoul(param(arg+2,argp),0,0); break;
    case 'c': {
     const char*k=param(arg+2,argp);
     const esp::Info1*info=esp::find1(k);	// "ESP" ⁿbergehen wenn angegeben
     if (!info) Fatal(0x104,k);			// "Unbekannter Chip %s"
     args.chip = info->image_chip_id;
    }break;
    case 'f': switch (arg[2]) {
     case 'm': args.flash_mode = char(find(param(arg+3,argp),fmode)); break;	// Flash-Modus
     case 's': args.flash_size = char(find(param(arg+3,argp),fsize)); break;	// Flash-Gr÷▀e
     case 'f': args.flash_freq = char(find(param(arg+3,argp),ffreq)); break;	// Flash-Frequenz
     default: Fatal(0x105,arg);			// "Unbekannte Option %s"
    }break;
    case 'i': args.input=param(arg+2,argp); break;
    case 'o': args.output=param(arg+2,argp); break;
    default: Fatal(0x105,arg);			// "Unbekannte Option %s"
   }break;
   default: if (operation) goto endparse;
   operation=op(find(arg,operations)+1);
   if (!operation) Fatal(0x106,arg);		// "Unbekannte Anweisung %s"
  }
 }
endparse:
/*
 parser = argparse.ArgumentParser(description='ESP CHIP ROM Bootloader Utility', prog='esptool')
  parser.add_argument("chip", 'c', help='CHIP_TYPE',choices=['ESP8266', 'ESP32'], default='ESP32')
  parser.add_argument("port", 'p', help='Serial port device', default='COM3')
  parser.add_argument("baud", 'b', help='Serial port baud rate',type=arg_auto_int,default=ESP::ROM_BAUD)
  subparsers = parser.add_subparsers(dest='operation',help='Run esptool {command} -h for additional help')
   parser_load_ram = subparsers.add_parser('load_ram',help='Download an image to RAM and execute')
    parser_load_ram.add_argument("filename", help='Firmware image')
   parser_dump_mem = subparsers.add_parser('dump_mem',help='Dump arbitrary memory to disk')
    parser_dump_mem.add_argument('address', help='Base address', type=arg_auto_int)
    parser_dump_mem.add_argument('size', help='Size of region to dump', type=arg_auto_int)
    parser_dump_mem.add_argument('filename', help='Name of binary dump')
   parser_read_mem = subparsers.add_parser('read_mem',help='Read arbitrary memory location')
    parser_read_mem.add_argument('address', help='Address to read', type=arg_auto_int)
   parser_write_mem = subparsers.add_parser('write_mem',help='Read-modify-write to arbitrary memory location')
    parser_write_mem.add_argument('address', help='Address to write', type=arg_auto_int)
    parser_write_mem.add_argument('value', help='Value', type=arg_auto_int)
    parser_write_mem.add_argument('mask', help='Mask of bits to write', type=arg_auto_int)
   parser_write_flash = subparsers.add_parser('write_flash',help='Write a binary blob to flash')
    parser_write_flash.add_argument('addr_filename', nargs='+', help='Address and binary file to write there, separated by space')
    parser_write_flash.add_argument("flash_freq", '-ff', help='SPI Flash frequency',choices=['40m', '26m', '20m', '80m'], default='40m')
    parser_write_flash.add_argument("flash_mode", '-fm', help='SPI Flash mode',choices=['qio', 'qout', 'dio', 'dout'], default='qio')
    parser_write_flash.add_argument("flash_size", '-fs', help='SPI Flash size in Mbit',choices=['1MB', '2MB','4MB', '8MB', '16MB'], default='1MB')
   subparsers.add_parser('run',help='Run application code in flash')
   parser_image_info = subparsers.add_parser('image_info',help='Dump headers from an application image')
    parser_image_info.add_argument('filename', help='Image file to parse')
   parser_make_image = subparsers.add_parser('make_image',help='Create an application image from binary files')
    parser_make_image.add_argument('output', help='Output image file')
    parser_make_image.add_argument("segfile", 'f', action='append', help='Segment input file')
    parser_make_image.add_argument("segaddr", 'a', action='append', help='Segment base address', type=arg_auto_int)
    parser_make_image.add_argument("entrypoint", 'e', help='Address of entry point', type=arg_auto_int, default=0)
   parser_elf2image = subparsers.add_parser('elf2image',help='Create an application image from ELF file')
    parser_elf2image.add_argument('input', help='Input ELF file')
    parser_elf2image.add_argument("output", 'o', help='Output filename prefix', type=str)
    parser_elf2image.add_argument("flash_freq", '-ff', help='SPI Flash frequency',choices=['40m', '26m', '20m', '80m'], default='40m')
    parser_elf2image.add_argument("flash_mode", '-fm', help='SPI Flash mode',choices=['qio', 'qout', 'dio', 'dout'], default='qio')
    parser_elf2image.add_argument("flash_size", '-fs', help='SPI Flash size in Mbit',choices=['1MB', '2MB', '4MB', '8MB', '16MB'], default='1MB')
   subparsers.add_parser('read_mac',help='Read MAC address from OTP ROM')
   subparsers.add_parser('flash_id',help='Read SPI flash manufacturer and device ID')
   parser_read_flash = subparsers.add_parser('read_flash',help='Read SPI flash content')
    parser_read_flash.add_argument('address', help='Start address', type=arg_auto_int)
    parser_read_flash.add_argument('size', help='Size of region to dump', type=arg_auto_int)
    parser_read_flash.add_argument('filename', help='Name of binary dump')
   subparsers.add_parser('erase_flash',help='Perform Chip Erase on SPI flash')
   subparsers.add_parser('test')
   args = parser.parse_args()
*/
 ESP*esp = 0;
 if (operation>=load_ram) {	// Serial connection needed
  for(;;) {
   esp = new ESP(comsel.port);
   if (esp->_port && esp->connect(comsel.baud)) break;
   delete esp; esp=0;
   if (!comsel.dialog()) Fatal(0x100);	// "Abbruch durch Benutzer"
  }
 }
    // Do the actual work.
 switch (operation) {
  case load_ram: {
   if (!*argp) Fatal(0x107);	// "Missing imgfilename"
   printf("RAM load\n");
   for (;;argp++) {
    const char*arg=*argp;
    if (!arg) break;
    parseBinFile(arg,ESP::verboseDownloadCb,esp,&args.entrypoint);
   }
   println("All segments done, executing at %08x",args.entrypoint);
   esp->mem_finish(args.entrypoint);
  }break;

  case read_mem: {
   if (!*argp) Fatal(0x119);	// "Missing list of addresses"
   for (;;argp++) {
    const char*arg=*argp;
    if (!arg) break;
    char*e;
    uint32 addr=strtoul(arg,&e,0);	// TODO: Detect malformed addresses; have a symbol list
    if (*e) continue;		// malformed
    println("%#x => %#x",addr,esp->read_reg(addr));
   }
  }break;

  case write_mem: {
   if (!*argp) Fatal(0x11A);	// "Missing list of addr:value[:mask]"
   for (;;argp++) {
    const char*arg=*argp;
    if (!arg) break;
    char*e;
    uint32 addr=strtoul(arg,&e,0);	// TODO: Detect malformed input
    if (*e!=':') continue;
    uint32 value=strtoul(e+1,&e,0),
           mask=-1;
    if (*e==':') mask=strtoul(e+1,&e,0);
    if (*e) continue;
    esp->write_reg(addr,value,mask,0);
    println("%#x <= %#x, mask %#x",addr,value,mask);
   }
  }break;

  case dump_mem: {
   if (!*argp) Fatal(0x109);	// "Missing list of [addr]Llen:binfilename[@hexaddr][.ext]"
   for (;;argp++) {
    const char*arg=*argp;
    if (!arg) break;
    Dumpfile df(arg);
    if (df.data()) {
     println("Won't overwrite file %s!",df.name());
     df.~df();
     continue;
    }
    if (df.flags&5 && df.flags&10) {	// sowohl Adresse als auch LΣnge vorgefunden
     FILE*f = fopen(df.name(),"wb");
     if (!f) {
      println("Cannot open/create %s for writing!",df.name());
      continue;
     }
     for (uint32 i=0; i<df.size; i+=4) {
      uint32 d = esp->read_reg(df.addr+i);
      fwrite(&d,1,4,f);
      if (!(i&1023)) {
       printf("\r%d bytes read... (%d %%)",i,i*100/df.size);
       fflush(stdout);
      }
     }
     fclose(f);
    }else println("Neither address nor length given at %s",arg);
   }
   println("Done!");
  }break;

  case write_flash: {
   if (!*argp) Fatal(0x108);	// "Missing list of [addr:]binfilename[@hexaddr][.ext]"
   if (!esp->load_stub()) break;
//   byte flash_info[2]={args.flash_mode,args.flash_size<<4|(args.flash_freq==3?15:args.flash_freq)};
   for (;;argp++) {
    const char*arg=*argp;
    if (!arg) break;
    parseBinFile(arg,ESP::write_flash_cb,esp,0);
   }
/*
   println("Leaving...");
   if (args.flash_mode==2) esp->flash_unlock_dio();	// 'dio'
   else {
    esp->flash_begin(0,0,false);
    esp->flash_finish(false,false);
   }
*/
  }break;

  case run: esp->run(); break;

  case image_info: {
   if (!*argp) Fatal(0x107);		// "Missing imgfilename"
   for (;;argp++) {
    const char*arg=*argp;
    if (!arg) break;
    printf("File %s:\n",arg);
    ESPFirmwareImage image(args.chip,arg);
    ESPFirmwareImage::IMGHDR&h=image.hdr;
    printf("Flash mode: %s, Flash frequency: %s, Flash size: %s,\n",
	image.fmode_str(),
	image.ffreq_str().c_str(),
	image.fsize_str().c_str());
//    printf("Image version %u, ",image.version());	// TODO: Don't know where to get it
    printf(h.entry?"Entry point: 0x%08X":"No entry point",h.entry);
    printf(", %u segments:\n", h.nseg);
    byte ck = esp::XOR8_INIT;
    for (int i=0; i<image.hdr.nseg; i++) {
     ESPFirmwareImage::SEG&cur=image.segments[i];
     printf("\tSegment #%u:%7u bytes (0x%05X) to target 0x%08X %s", i, cur.size, cur.size, cur.offset,
	esp::memareas(image.m_chip,cur.offset,cur.offset+cur.size).c_str());
     putchar('\n');
     ck = esp::xor8(cur.data, cur.size, ck);
    }
    if (image.check_avail) printf("Checksum: %02x (%s)\n", image.check, image.check == ck ? "valid" : "invalid!");
    if (h.hash_append==1) {
     bool equal=image.filehash==image.calchash;
     printf("SHA-256 hash: %s (%s)\n",image.filehash.toString().c_str(),equal ? "valid" : "invalid!");
     if (!equal) {
      printf("file hash is: %s\n",image.calchash.toString().c_str());
     }	// Interessant! .c_str() ist nicht erforderlich, da kommt das gleiche 'raus
    }
   }
  }break;

  case make_image: {
   if (!*argp) Fatal(0x108);		// "Missing list of [addr:]binfilename[@hexaddr][.ext]"
   if (!args.output) Fatal("No output filename given, write to stdout not supported (GetSaveFileName()?)");
   ESPFirmwareImage image(args.chip);
// Leerzeichen erlaubt wenn entsprechend "escaped"
   for (;;argp++) {
    const char*arg=*argp;
    if (!arg) break;
// TODO: Hier nicht zerlegen, einfach as-is laden und Zwieback akzeptieren
// TODO: ▄berlappungen ⁿberwachen (das darf nicht passieren)
    parseBinFile(arg,ESPFirmwareImage::add_segment_cb,&image,&args.entrypoint);
   }
   image.hdr.entry = args.entrypoint;
   image.save(args.output);
  }break;

  case elf2image: {
   if (!args.output) {
    size_t li = strlen(args.input);
    char*p = new char[li+2];
    memcpy(p,args.input,li);
    p[li++]='-';
    p[li]=0;
    args.output = p;
   }
   ELFFile e(args.input);
   ESPFirmwareImage image(args.chip);
   image.hdr.entry = e.get_entry_point();
   static const char*const sections[]={"text","data","rodata",0};
   for (const char*const*sn=sections;*sn;sn++) {
    char s[16];
    _snprintf(s,sizeof s,".%s",*sn);
    const void*data;
    uint32 dlen = e.load_section(s,data);
    _snprintf(s,sizeof s,"_%s_start",*sn);
    Dumpfile sub((byte*)data,dlen,e.get_symbol_addr(s));
    image.add_segment(sub);
    delete[] const_cast<void*>(data);
   }
   image.hdr.fmode = args.flash_mode;
   image.hdr.ffreq = args.flash_freq==3?15:args.flash_freq;
   image.hdr.fsize = args.flash_size;

   char s[256];
   _snprintf(s,sizeof s,"%s@%X.bin",args.output,0);
   image.save(s);
   const void*data;
   uint32 dlen = e.load_section(".irom0.text",data);
   uint32 off = e.get_symbol_addr("_irom0_text_start") - 0x40200000;
//   assert off >= 0
   _snprintf(s,sizeof s,"%s@%X.bin",args.output,off);
   FILE*f = fopen(s,"wb");
   fwrite(data,1,dlen,f);
   fclose(f);
   delete[] const_cast<void*>(data);
  }break;

  case read_mac: {	// macht gar nichts! Die MAC-Adresse wird immer angezeigt!
  }break;

  case flash_id: switch (esp->info1->image_chip_id) {
   case 32: printf("SUPPORT ESP32 LATER"); break;
   case 66: {
    uint32 id = esp->flash_id();
    printf("Manufacturer: %02x", byte(id));
    printf("Device: %02x%02x",byte(id>>8),byte(id>>16));
   }break;
  }break;

  case read_flash: {
   if (!*argp) Fatal(0x109);	// "Missing list of [addr]Llen:binfilename[@hexaddr][.ext]"
   if (!esp->load_stub()) break;
   for (;;argp++) {
    const char*arg=*argp;
    if (!arg) break;
    if (!esp->flash2file(arg)) break;
   }
  }break;

  case erase_flash: {
   if (esp->load_stub()) esp->flash_erase(); 
  }break;

  case verify_flash: {
   if (!*argp) Fatal(0x108);	// "Missing list of [addr:]binfilename[@hexaddr][.ext]"
   esp->load_stub();		// laut Datenblatt unn÷tig, aber es geht (bei mir) nicht
   for (;;argp++) {
    const char*arg=*argp;
    if (!arg) break;
    if (!esp->flash_verify(arg)) break;
   }
  }break;
    
  case test: {
        //esp->flash_begin(0x1000,0x200000)
   byte res = esp->check_crc();
   printf("res:",res);
  }break;
  
  case none: {
   println(0x117);	// "usage: esptool [-h] [-c ESPxxx] [-p COMx] [-b BAUD] action ..."
   printf(0x118);	// "actions: "
   putchar('{'); ++tty.x;
   for (const char*po=operations,*poe=po;*po;po=poe) {
    poe=po+strlen(po)+1;
    if (tty.x+poe-po+1>=tty.w-1) newline();	// Kein Platz fⁿr das nΣchste (Wort+Komma) auf der Zeile?
    tty.x+=printf("%s%c",po,*poe?',':'}');
   }
   newline();
  }break;

  default: Fatal(0x101,*--argp);	// not yet implemented
 }
 delete esp;	// sonst kein Destruktoraufruf
 return 0;
}

#ifdef _MSC_VER
extern "C" _CRTIMP int _cdecl __getmainargs(int&,char**&,char**&,int,int&);

int mainCRTStartup() {
 int argc, newmode;
 char**argv,**envp;
 __getmainargs(argc,argv,envp,true,newmode);
 ExitProcess(main(argc,argv));
}
#endif
Detected encoding: OEM (CP437)1
Wrong umlauts? - Assume file is ANSI (CP1252) encoded