int SFD_DumpSplineFontMetadata( FILE *sfd, SplineFont *sf )
{
    int i, j;
    struct ttflangname *ln;
    struct ttf_table *tab;
    KernClass *kc;
    FPST *fpst;
    ASM *sm;
    int isv;
    int err = false;
    int isgpos;
    OTLookup *otl;
    struct lookup_subtable *sub;
    FeatureScriptLangList *fl;
    struct scriptlanglist *sl;

    fprintf(sfd, "FontName: %s\n", sf->fontname );
    if ( sf->fullname!=NULL )
	fprintf(sfd, "FullName: %s\n", sf->fullname );
    if ( sf->familyname!=NULL )
	fprintf(sfd, "FamilyName: %s\n", sf->familyname );
    if ( sf->weight!=NULL )
	fprintf(sfd, "Weight: %s\n", sf->weight );
    if ( sf->copyright!=NULL )
	putstring(sfd, "Copyright: ", sf->copyright );
    if ( sf->comments!=NULL ) {
	fprintf( sfd, "UComments: " );
	SFDDumpUTF7Str(sfd,sf->comments);
	putc('\n',sfd);
    }
    if ( sf->fontlog!=NULL ) {
	fprintf( sfd, "FontLog: " );
	SFDDumpUTF7Str(sfd,sf->fontlog);
	putc('\n',sfd);
    }

    if ( sf->version!=NULL )
	fprintf(sfd, "Version: %s\n", sf->version );
    if ( sf->styleMapFamilyName!=NULL )
	fprintf(sfd, "StyleMapFamilyName: %s\n", sf->styleMapFamilyName );
    if ( sf->fondname!=NULL )
	fprintf(sfd, "FONDName: %s\n", sf->fondname );
    if ( sf->defbasefilename!=NULL )
	fprintf(sfd, "DefaultBaseFilename: %s\n", sf->defbasefilename );
    if ( sf->strokewidth!=0 )
	fprintf(sfd, "StrokeWidth: %g\n", (double) sf->strokewidth );
    fprintf(sfd, "ItalicAngle: %g\n", (double) sf->italicangle );
    fprintf(sfd, "UnderlinePosition: %g\n", (double) sf->upos );
    fprintf(sfd, "UnderlineWidth: %g\n", (double) sf->uwidth );
    fprintf(sfd, "Ascent: %d\n", sf->ascent );
    fprintf(sfd, "Descent: %d\n", sf->descent );
    fprintf(sfd, "InvalidEm: %d\n", sf->invalidem );
    if ( sf->sfntRevision!=sfntRevisionUnset )
	fprintf(sfd, "sfntRevision: 0x%08x\n", sf->sfntRevision );
    if ( sf->woffMajor!=woffUnset ) {
	fprintf(sfd, "woffMajor: %d\n", sf->woffMajor );
	fprintf(sfd, "woffMinor: %d\n", sf->woffMinor );
    }
    if ( sf->woffMetadata!=NULL ) {
	fprintf( sfd, "woffMetadata: " );
	SFDDumpUTF7Str(sfd,sf->woffMetadata);
	putc('\n',sfd);
    }
    if ( sf->ufo_ascent!=0 )
	fprintf(sfd, "UFOAscent: %g\n", (double) sf->ufo_ascent );
    if ( sf->ufo_descent!=0 )
	fprintf(sfd, "UFODescent: %g\n", (double) sf->ufo_descent );
    fprintf(sfd, "LayerCount: %d\n", sf->layer_cnt );
    for ( i=0; i<sf->layer_cnt; ++i ) {
	fprintf( sfd, "Layer: %d %d ", i, sf->layers[i].order2/*, sf->layers[i].background*/ );
	SFDDumpUTF7Str(sfd,sf->layers[i].name);
	fprintf( sfd, " %d", sf->layers[i].background );
	if (sf->layers[i].ufo_path != NULL) { putc(' ',sfd); SFDDumpUTF7Str(sfd,sf->layers[i].ufo_path); }
	putc('\n',sfd);
    }
    // TODO: Output U. F. O. layer path.
    if (sf->preferred_kerning != 0) fprintf(sfd, "PreferredKerning: %d\n", sf->preferred_kerning);
    if ( sf->strokedfont )
	fprintf(sfd, "StrokedFont: %d\n", sf->strokedfont );
    else if ( sf->multilayer )
	fprintf(sfd, "MultiLayer: %d\n", sf->multilayer );
    if ( sf->hasvmetrics )
	fprintf(sfd, "HasVMetrics: %d\n", sf->hasvmetrics );
    if ( sf->use_xuid && sf->changed_since_xuidchanged )
	fprintf(sfd, "NeedsXUIDChange: 1\n" );
    if ( sf->xuid!=NULL )
	fprintf(sfd, "XUID: %s\n", sf->xuid );
    if ( sf->uniqueid!=0 )
	fprintf(sfd, "UniqueID: %d\n", sf->uniqueid );
    if ( sf->use_xuid )
	fprintf(sfd, "UseXUID: 1\n" );
    if ( sf->use_uniqueid )
	fprintf(sfd, "UseUniqueID: 1\n" );
    if ( sf->horiz_base!=NULL )
	SFDDumpBase(sfd,"BaseHoriz:",sf->horiz_base);
    if ( sf->vert_base!=NULL )
	SFDDumpBase(sfd,"BaseVert:",sf->vert_base);
    if ( sf->pfminfo.stylemap!=-1 )
	fprintf(sfd, "StyleMap: 0x%04x\n", sf->pfminfo.stylemap );
    if ( sf->pfminfo.fstype!=-1 )
	fprintf(sfd, "FSType: %d\n", sf->pfminfo.fstype );
    fprintf(sfd, "OS2Version: %d\n", sf->os2_version );
    fprintf(sfd, "OS2_WeightWidthSlopeOnly: %d\n", sf->weight_width_slope_only );
    fprintf(sfd, "OS2_UseTypoMetrics: %d\n", sf->use_typo_metrics );
    fprintf(sfd, "CreationTime: %lld\n", sf->creationtime );
    fprintf(sfd, "ModificationTime: %lld\n", sf->modificationtime );
    if ( sf->pfminfo.pfmset ) {
	fprintf(sfd, "PfmFamily: %d\n", sf->pfminfo.pfmfamily );
	fprintf(sfd, "TTFWeight: %d\n", sf->pfminfo.weight );
	fprintf(sfd, "TTFWidth: %d\n", sf->pfminfo.width );
	fprintf(sfd, "LineGap: %d\n", sf->pfminfo.linegap );
	fprintf(sfd, "VLineGap: %d\n", sf->pfminfo.vlinegap );
	/*putc('\n',sfd);*/
    }
    if ( sf->pfminfo.panose_set ) {
	fprintf(sfd, "Panose:" );
	for ( i=0; i<10; ++i )
	    fprintf( sfd, " %d", sf->pfminfo.panose[i]);
	putc('\n',sfd);
    }
    fprintf(sfd, "OS2TypoAscent: %d\n", sf->pfminfo.os2_typoascent );
    fprintf(sfd, "OS2TypoAOffset: %d\n", sf->pfminfo.typoascent_add );
    fprintf(sfd, "OS2TypoDescent: %d\n", sf->pfminfo.os2_typodescent );
    fprintf(sfd, "OS2TypoDOffset: %d\n", sf->pfminfo.typodescent_add );
    fprintf(sfd, "OS2TypoLinegap: %d\n", sf->pfminfo.os2_typolinegap );
    fprintf(sfd, "OS2WinAscent: %d\n", sf->pfminfo.os2_winascent );
    fprintf(sfd, "OS2WinAOffset: %d\n", sf->pfminfo.winascent_add );
    fprintf(sfd, "OS2WinDescent: %d\n", sf->pfminfo.os2_windescent );
    fprintf(sfd, "OS2WinDOffset: %d\n", sf->pfminfo.windescent_add );
    fprintf(sfd, "HheadAscent: %d\n", sf->pfminfo.hhead_ascent );
    fprintf(sfd, "HheadAOffset: %d\n", sf->pfminfo.hheadascent_add );
    fprintf(sfd, "HheadDescent: %d\n", sf->pfminfo.hhead_descent );
    fprintf(sfd, "HheadDOffset: %d\n", sf->pfminfo.hheaddescent_add );
    if ( sf->pfminfo.subsuper_set ) {
	fprintf(sfd, "OS2SubXSize: %d\n", sf->pfminfo.os2_subxsize );
	fprintf(sfd, "OS2SubYSize: %d\n", sf->pfminfo.os2_subysize );
	fprintf(sfd, "OS2SubXOff: %d\n", sf->pfminfo.os2_subxoff );
	fprintf(sfd, "OS2SubYOff: %d\n", sf->pfminfo.os2_subyoff );
	fprintf(sfd, "OS2SupXSize: %d\n", sf->pfminfo.os2_supxsize );
	fprintf(sfd, "OS2SupYSize: %d\n", sf->pfminfo.os2_supysize );
	fprintf(sfd, "OS2SupXOff: %d\n", sf->pfminfo.os2_supxoff );
	fprintf(sfd, "OS2SupYOff: %d\n", sf->pfminfo.os2_supyoff );
	fprintf(sfd, "OS2StrikeYSize: %d\n", sf->pfminfo.os2_strikeysize );
	fprintf(sfd, "OS2StrikeYPos: %d\n", sf->pfminfo.os2_strikeypos );
    }
    if ( sf->pfminfo.os2_capheight!=0 )
    fprintf(sfd, "OS2CapHeight: %d\n", sf->pfminfo.os2_capheight );
    if ( sf->pfminfo.os2_xheight!=0 )
    fprintf(sfd, "OS2XHeight: %d\n", sf->pfminfo.os2_xheight );
    if ( sf->pfminfo.os2_family_class!=0 )
	fprintf(sfd, "OS2FamilyClass: %d\n", sf->pfminfo.os2_family_class );
    if ( sf->pfminfo.os2_vendor[0]!='\0' ) {
	fprintf(sfd, "OS2Vendor: '%c%c%c%c'\n",
		sf->pfminfo.os2_vendor[0], sf->pfminfo.os2_vendor[1],
		sf->pfminfo.os2_vendor[2], sf->pfminfo.os2_vendor[3] );
    }
    if ( sf->pfminfo.hascodepages )
	fprintf(sfd, "OS2CodePages: %08x.%08x\n", sf->pfminfo.codepages[0], sf->pfminfo.codepages[1]);
    if ( sf->pfminfo.hasunicoderanges )
	fprintf(sfd, "OS2UnicodeRanges: %08x.%08x.%08x.%08x\n",
		sf->pfminfo.unicoderanges[0], sf->pfminfo.unicoderanges[1],
		sf->pfminfo.unicoderanges[2], sf->pfminfo.unicoderanges[3] );
    if ( sf->macstyle!=-1 )
	fprintf(sfd, "MacStyle: %d\n", sf->macstyle );
    /* Must come before any kerning classes, anchor classes, conditional psts */
    /* state machines, psts, kerning pairs, etc. */
    for ( isgpos=0; isgpos<2; ++isgpos ) {
	for ( otl = isgpos ? sf->gpos_lookups : sf->gsub_lookups; otl!=NULL; otl = otl->next ) {
	    fprintf( sfd, "Lookup: %d %d %d ", otl->lookup_type, otl->lookup_flags, otl->store_in_afm );
	    SFDDumpUTF7Str(sfd,otl->lookup_name);
	    fprintf( sfd, " { " );
	    for ( sub=otl->subtables; sub!=NULL; sub=sub->next ) {
		SFDDumpUTF7Str(sfd,sub->subtable_name);
		putc(' ',sfd);
		if ( otl->lookup_type==gsub_single && sub->suffix!=NULL ) {
		    putc('(',sfd);
		    SFDDumpUTF7Str(sfd,sub->suffix);
		    putc(')',sfd);
		} else if ( otl->lookup_type==gpos_pair && sub->vertical_kerning )
		    fprintf(sfd,"(1)");
		if ( otl->lookup_type==gpos_pair && (sub->separation!=0 || sub->kerning_by_touch))
		    fprintf(sfd,"[%d,%d,%d]", sub->separation, sub->minkern, sub->kerning_by_touch+2*sub->onlyCloser+4*sub->dontautokern );
		putc(' ',sfd);
	    }
	    fprintf( sfd, "} [" );
	    for ( fl=otl->features; fl!=NULL; fl=fl->next ) {
		if ( fl->ismac )
		    fprintf( sfd, "<%d,%d> (",
			    (int) (fl->featuretag>>16),
			    (int) (fl->featuretag&0xffff));
		else
		    fprintf( sfd, "'%c%c%c%c' (",
			    (int) (fl->featuretag>>24), (int) ((fl->featuretag>>16)&0xff),
			    (int) ((fl->featuretag>>8)&0xff), (int) (fl->featuretag&0xff) );
		for ( sl= fl->scripts; sl!=NULL; sl = sl->next ) {
		    fprintf( sfd, "'%c%c%c%c' <",
			    (int) (sl->script>>24), (int) ((sl->script>>16)&0xff),
			    (int) ((sl->script>>8)&0xff), (int) (sl->script&0xff) );
		    for ( i=0; i<sl->lang_cnt; ++i ) {
			uint32 lang = i<MAX_LANG ? sl->langs[i] : sl->morelangs[i-MAX_LANG];
			fprintf( sfd, "'%c%c%c%c' ",
				(int) (lang>>24), (int) ((lang>>16)&0xff),
				(int) ((lang>>8)&0xff), (int) (lang&0xff) );
		    }
		    fprintf( sfd, "> " );
		}
		fprintf( sfd, ") " );
	    }
	    fprintf( sfd, "]\n" );
	}
    }

    if ( sf->mark_class_cnt!=0 ) {
	fprintf( sfd, "MarkAttachClasses: %d\n", sf->mark_class_cnt );
	for ( i=1; i<sf->mark_class_cnt; ++i ) {	/* Class 0 is unused */
	    SFDDumpUTF7Str(sfd, sf->mark_class_names[i]);
	    putc(' ',sfd);
	    if ( sf->mark_classes[i]!=NULL )
		fprintf( sfd, "%d %s\n", (int) strlen(sf->mark_classes[i]),
			sf->mark_classes[i] );
	    else
		fprintf( sfd, "0 \n" );
	}
    }
    if ( sf->mark_set_cnt!=0 ) {
	fprintf( sfd, "MarkAttachSets: %d\n", sf->mark_set_cnt );
	for ( i=0; i<sf->mark_set_cnt; ++i ) {	/* Set 0 is used */
	    SFDDumpUTF7Str(sfd, sf->mark_set_names[i]);
	    putc(' ',sfd);
	    if ( sf->mark_sets[i]!=NULL )
		fprintf( sfd, "%d %s\n", (int) strlen(sf->mark_sets[i]),
			sf->mark_sets[i] );
	    else
		fprintf( sfd, "0 \n" );
	}
    }

    fprintf( sfd, "DEI: 91125\n" );
    for ( isv=0; isv<2; ++isv ) {
	for ( kc=isv ? sf->vkerns : sf->kerns; kc!=NULL; kc = kc->next ) {
	  if (kc->firsts_names == NULL && kc->seconds_names == NULL && kc->firsts_flags == NULL && kc->seconds_flags == NULL) {
	    fprintf( sfd, "%s: %d%s %d ", isv ? "VKernClass2" : "KernClass2",
		    kc->first_cnt, kc->firsts[0]!=NULL?"+":"",
		    kc->second_cnt );
	    SFDDumpUTF7Str(sfd,kc->subtable->subtable_name);
	    putc('\n',sfd);
	    if ( kc->firsts[0]!=NULL )
	      fprintf( sfd, " %d %s\n", (int)strlen(kc->firsts[0]),
		       kc->firsts[0]);
	    for ( i=1; i<kc->first_cnt; ++i )
	      fprintf( sfd, " %d %s\n", (int)strlen(kc->firsts[i]),
		       kc->firsts[i]);
	    for ( i=1; i<kc->second_cnt; ++i )
	      fprintf( sfd, " %d %s\n", (int)strlen(kc->seconds[i]),
		       kc->seconds[i]);
	    for ( i=0; i<kc->first_cnt*kc->second_cnt; ++i ) {
		fprintf( sfd, " %d", kc->offsets[i]);
		putc(' ',sfd);
		SFDDumpDeviceTable(sfd,&kc->adjusts[i]);
	    }
	    fprintf( sfd, "\n" );
	  } else {
	    fprintf( sfd, "%s: %d%s %d ", isv ? "VKernClass3" : "KernClass3",
		    kc->first_cnt, kc->firsts[0]!=NULL?"+":"",
		    kc->second_cnt );
	    SFDDumpUTF7Str(sfd,kc->subtable->subtable_name);
	    putc('\n',sfd);
	    if ( kc->firsts[0]!=NULL ) {
	      fprintf( sfd, " %d ", ((kc->firsts_flags && kc->firsts_flags[0]) ? kc->firsts_flags[0] : 0));
	      SFDDumpUTF7Str(sfd, ((kc->firsts_names && kc->firsts_names[0]) ? kc->firsts_names[0] : ""));
	      fprintf( sfd, " " );
	      SFDDumpUTF7Str(sfd,kc->firsts[0]);
	      fprintf( sfd, "\n" );
	    }
	    for ( i=1; i<kc->first_cnt; ++i ) {
	      fprintf( sfd, " %d ", ((kc->firsts_flags && kc->firsts_flags[i]) ? kc->firsts_flags[i] : 0));
	      SFDDumpUTF7Str(sfd, ((kc->firsts_names && kc->firsts_names[i]) ? kc->firsts_names[i] : ""));
	      fprintf( sfd, " " );
	      SFDDumpUTF7Str(sfd,kc->firsts[i]);
	      fprintf( sfd, "\n" );
	    }
	    for ( i=1; i<kc->second_cnt; ++i ) {
	      fprintf( sfd, " %d ", ((kc->seconds_flags && kc->seconds_flags[i]) ? kc->seconds_flags[i] : 0));
	      SFDDumpUTF7Str(sfd, ((kc->seconds_names && kc->seconds_names[i]) ? kc->seconds_names[i] : ""));
	      fprintf( sfd, " " );
	      SFDDumpUTF7Str(sfd,kc->seconds[i]);
	      fprintf( sfd, "\n" );
	    }
	    for ( i=0; i<kc->first_cnt*kc->second_cnt; ++i ) {
		fprintf( sfd, " %d %d", ((kc->offsets_flags && kc->offsets_flags[i]) ? kc->offsets_flags[i] : 0), kc->offsets[i]);
		putc(' ',sfd);
		SFDDumpDeviceTable(sfd,&kc->adjusts[i]);
	    }
	    fprintf( sfd, "\n" );
	  }
	}
    }
    for ( fpst=sf->possub; fpst!=NULL; fpst=fpst->next ) {
	static const char *keywords[] = { "ContextPos2:", "ContextSub2:", "ChainPos2:", "ChainSub2:", "ReverseChain2:", NULL };
	static const char *formatkeys[] = { "glyph", "class", "coverage", "revcov", NULL };
	fprintf( sfd, "%s %s ", keywords[fpst->type-pst_contextpos],
		formatkeys[fpst->format] );
	SFDDumpUTF7Str(sfd,fpst->subtable->subtable_name);
	fprintf( sfd, " %d %d %d %d\n",
		fpst->nccnt, fpst->bccnt, fpst->fccnt, fpst->rule_cnt );
	if ( fpst->nccnt>0 && fpst->nclass[0]!=NULL )
	  fprintf( sfd, "  Class0: %d %s\n", (int)strlen(fpst->nclass[0]),
		   fpst->nclass[0]);
	for ( i=1; i<fpst->nccnt; ++i )
	  fprintf( sfd, "  Class: %d %s\n", (int)strlen(fpst->nclass[i]),
		   fpst->nclass[i]);
	for ( i=1; i<fpst->bccnt; ++i )
	  fprintf( sfd, "  BClass: %d %s\n", (int)strlen(fpst->bclass[i]),
		   fpst->bclass[i]);
	for ( i=1; i<fpst->fccnt; ++i )
	  fprintf( sfd, "  FClass: %d %s\n", (int)strlen(fpst->fclass[i]),
		   fpst->fclass[i]);
	for ( i=0; i<fpst->rule_cnt; ++i ) {
	    switch ( fpst->format ) {
	      case pst_glyphs:
		fprintf( sfd, " String: %d %s\n",
			 (int)strlen(fpst->rules[i].u.glyph.names),
			 fpst->rules[i].u.glyph.names);
		if ( fpst->rules[i].u.glyph.back!=NULL )
		  fprintf( sfd, " BString: %d %s\n",
			   (int)strlen(fpst->rules[i].u.glyph.back),
			   fpst->rules[i].u.glyph.back);
		else
		    fprintf( sfd, " BString: 0\n");
		if ( fpst->rules[i].u.glyph.fore!=NULL )
		  fprintf( sfd, " FString: %d %s\n",
			   (int)strlen(fpst->rules[i].u.glyph.fore),
			   fpst->rules[i].u.glyph.fore);
		else
		    fprintf( sfd, " FString: 0\n");
	      break;
	      case pst_class:
		fprintf( sfd, " %d %d %d\n  ClsList:", fpst->rules[i].u.class.ncnt, fpst->rules[i].u.class.bcnt, fpst->rules[i].u.class.fcnt );
		for ( j=0; j<fpst->rules[i].u.class.ncnt; ++j )
		    fprintf( sfd, " %d", fpst->rules[i].u.class.nclasses[j]);
		fprintf( sfd, "\n  BClsList:" );
		for ( j=0; j<fpst->rules[i].u.class.bcnt; ++j )
		    fprintf( sfd, " %d", fpst->rules[i].u.class.bclasses[j]);
		fprintf( sfd, "\n  FClsList:" );
		for ( j=0; j<fpst->rules[i].u.class.fcnt; ++j )
		    fprintf( sfd, " %d", fpst->rules[i].u.class.fclasses[j]);
		fprintf( sfd, "\n" );
	      break;
	      case pst_coverage:
	      case pst_reversecoverage:
		fprintf( sfd, " %d %d %d\n", fpst->rules[i].u.coverage.ncnt, fpst->rules[i].u.coverage.bcnt, fpst->rules[i].u.coverage.fcnt );
		for ( j=0; j<fpst->rules[i].u.coverage.ncnt; ++j )
		  fprintf( sfd, "  Coverage: %d %s\n",
			   (int)strlen(fpst->rules[i].u.coverage.ncovers[j]),
			   fpst->rules[i].u.coverage.ncovers[j]);
		for ( j=0; j<fpst->rules[i].u.coverage.bcnt; ++j )
		  fprintf( sfd, "  BCoverage: %d %s\n",
			   (int)strlen(fpst->rules[i].u.coverage.bcovers[j]),
			   fpst->rules[i].u.coverage.bcovers[j]);
		for ( j=0; j<fpst->rules[i].u.coverage.fcnt; ++j )
		  fprintf( sfd, "  FCoverage: %d %s\n",
			   (int)strlen(fpst->rules[i].u.coverage.fcovers[j]),
			   fpst->rules[i].u.coverage.fcovers[j]);
	      break;
	      default:
	      break;
	    }
	    switch ( fpst->format ) {
	      case pst_glyphs:
	      case pst_class:
	      case pst_coverage:
		fprintf( sfd, " %d\n", fpst->rules[i].lookup_cnt );
		for ( j=0; j<fpst->rules[i].lookup_cnt; ++j ) {
		    fprintf( sfd, "  SeqLookup: %d ",
			    fpst->rules[i].lookups[j].seq );
		    SFDDumpUTF7Str(sfd,fpst->rules[i].lookups[j].lookup->lookup_name);
		    putc('\n',sfd);
		}
	      break;
	      case pst_reversecoverage:
		fprintf( sfd, "  Replace: %d %s\n",
			 (int)strlen(fpst->rules[i].u.rcoverage.replacements),
			 fpst->rules[i].u.rcoverage.replacements);
	      break;
	      default:
	      break;
	    }
	}
	/* It would make more sense to output these up near the classes */
	/*  but that would break backwards compatibility (old parsers will */
	/*  ignore these entries if they are at the end, new parsers will */
	/*  read them */
	SFDFpstClassNamesOut(sfd,fpst->nccnt,fpst->nclassnames,"ClassNames");
	SFDFpstClassNamesOut(sfd,fpst->bccnt,fpst->bclassnames,"BClassNames");
	SFDFpstClassNamesOut(sfd,fpst->fccnt,fpst->fclassnames,"FClassNames");
	fprintf( sfd, "EndFPST\n" );
    }
    struct ff_glyphclasses *grouptmp;
    for ( grouptmp = sf->groups; grouptmp != NULL; grouptmp = grouptmp->next ) {
      fprintf(sfd, "Group: ");
      SFDDumpUTF7Str(sfd, grouptmp->classname); fprintf(sfd, " ");
      SFDDumpUTF7Str(sfd, grouptmp->glyphs); fprintf(sfd, "\n");
    }
    struct ff_rawoffsets *groupkerntmp;
    for ( groupkerntmp = sf->groupkerns; groupkerntmp != NULL; groupkerntmp = groupkerntmp->next ) {
      fprintf(sfd, "GroupKern: ");
      SFDDumpUTF7Str(sfd, groupkerntmp->left); fprintf(sfd, " ");
      SFDDumpUTF7Str(sfd, groupkerntmp->right); fprintf(sfd, " ");
      fprintf(sfd, "%d\n", groupkerntmp->offset);
    }
    for ( groupkerntmp = sf->groupvkerns; groupkerntmp != NULL; groupkerntmp = groupkerntmp->next ) {
      fprintf(sfd, "GroupVKern: ");
      SFDDumpUTF7Str(sfd, groupkerntmp->left); fprintf(sfd, " ");
      SFDDumpUTF7Str(sfd, groupkerntmp->right); fprintf(sfd, " ");
      fprintf(sfd, "%d\n", groupkerntmp->offset);
    }
    for ( sm=sf->sm; sm!=NULL; sm=sm->next ) {
	static const char *keywords[] = { "MacIndic2:", "MacContext2:", "MacLigature2:", "unused", "MacSimple2:", "MacInsert2:",
	    "unused", "unused", "unused", "unused", "unused", "unused",
	    "unused", "unused", "unused", "unused", "unused", "MacKern2:",
	    NULL };
	fprintf( sfd, "%s ", keywords[sm->type-asm_indic] );
	SFDDumpUTF7Str(sfd,sm->subtable->subtable_name);
	fprintf( sfd, " %d %d %d\n", sm->flags, sm->class_cnt, sm->state_cnt );
	for ( i=4; i<sm->class_cnt; ++i )
	  fprintf( sfd, "  Class: %d %s\n", (int)strlen(sm->classes[i]),
		   sm->classes[i]);
	for ( i=0; i<sm->class_cnt*sm->state_cnt; ++i ) {
	    fprintf( sfd, " %d %d ", sm->state[i].next_state, sm->state[i].flags );
	    if ( sm->type==asm_context ) {
		if ( sm->state[i].u.context.mark_lookup==NULL )
		    putc('~',sfd);
		else
		    SFDDumpUTF7Str(sfd,sm->state[i].u.context.mark_lookup->lookup_name);
		putc(' ',sfd);
		if ( sm->state[i].u.context.cur_lookup==0 )
		    putc('~',sfd);
		else
		    SFDDumpUTF7Str(sfd,sm->state[i].u.context.cur_lookup->lookup_name);
		putc(' ',sfd);
	    } else if ( sm->type == asm_insert ) {
		if ( sm->state[i].u.insert.mark_ins==NULL )
		    fprintf( sfd, "0 ");
		else
		  fprintf( sfd, "%d %s ",
			   (int)strlen(sm->state[i].u.insert.mark_ins),
			   sm->state[i].u.insert.mark_ins );
		if ( sm->state[i].u.insert.cur_ins==NULL )
		    fprintf( sfd, "0 ");
		else
		  fprintf( sfd, "%d %s ",
			   (int)strlen(sm->state[i].u.insert.cur_ins),
			   sm->state[i].u.insert.cur_ins );
	    } else if ( sm->type == asm_kern ) {
		fprintf( sfd, "%d ", sm->state[i].u.kern.kcnt );
		for ( j=0; j<sm->state[i].u.kern.kcnt; ++j )
		    fprintf( sfd, "%d ", sm->state[i].u.kern.kerns[j]);
	    }
	    putc('\n',sfd);
	}
	fprintf( sfd, "EndASM\n" );
    }
    SFDDumpMacFeat(sfd,sf->features);
    SFDDumpJustify(sfd,sf);
    for ( tab = sf->ttf_tables; tab!=NULL ; tab = tab->next )
	SFDDumpTtfTable(sfd,tab,sf);
    for ( tab = sf->ttf_tab_saved; tab!=NULL ; tab = tab->next )
	SFDDumpTtfTable(sfd,tab,sf);
    for ( ln = sf->names; ln!=NULL; ln=ln->next )
	SFDDumpLangName(sfd,ln);
    if ( sf->gasp_cnt!=0 )
	SFDDumpGasp(sfd,sf);
    if ( sf->design_size!=0 )
	SFDDumpDesignSize(sfd,sf);
    if ( sf->feat_names!=NULL )
	SFDDumpOtfFeatNames(sfd,sf);

return( err );
}


static void SFDDumpGasp(FILE *sfd, SplineFont *sf) {
    int i;

    if ( sf->gasp_cnt==0 )
return;

    fprintf( sfd, "GaspTable: %d", sf->gasp_cnt );
    for ( i=0; i<sf->gasp_cnt; ++i )
	fprintf( sfd, " %d %d", sf->gasp[i].ppem, sf->gasp[i].flags );
    fprintf( sfd, " %d", sf->gasp_version);
    putc('\n',sfd);
}

Fix for #4084 Use-after-free (heap) in the SFD_GetFontMetaData() function
Fix for #4086 NULL pointer dereference in the SFDGetSpiros() function
Fix for #4088 NULL pointer dereference in the SFD_AssignLookups() function
Add empty sf->fontname string if it isn't set, fixing #4089 #4090 and many
  other potential issues (many downstream calls to strlen() on the value).


static void SFDParseVertexKern(FILE *sfd, struct mathkernvertex *vertex) {
    int i,ch;

    getint(sfd,&vertex->cnt);
    vertex->mkd = calloc(vertex->cnt,sizeof(struct mathkerndata));
    for ( i=0; i<vertex->cnt; ++i ) {
	SFDParseMathValueRecord(sfd,&vertex->mkd[i].height,&vertex->mkd[i].height_adjusts);
	while ( (ch=nlgetc(sfd))==' ' );
	if ( ch!=EOF && ch!=',' )
	    ungetc(ch,sfd);
	SFDParseMathValueRecord(sfd,&vertex->mkd[i].kern,&vertex->mkd[i].kern_adjusts);
    }
}


static void SFDParseChainContext(FILE *sfd,SplineFont *sf,FPST *fpst, char *tok, int old) {
    int ch, i, j, k, temp;
    SplineFont *sli_sf = sf->cidmaster ? sf->cidmaster : sf;

    fpst->type = strnmatch(tok,"ContextPos",10)==0 ? pst_contextpos :
		strnmatch(tok,"ContextSub",10)==0 ? pst_contextsub :
		strnmatch(tok,"ChainPos",8)==0 ? pst_chainpos :
		strnmatch(tok,"ChainSub",8)==0 ? pst_chainsub : pst_reversesub;
    getname(sfd,tok);
    fpst->format = strmatch(tok,"glyph")==0 ? pst_glyphs :
		    strmatch(tok,"class")==0 ? pst_class :
		    strmatch(tok,"coverage")==0 ? pst_coverage : pst_reversecoverage;
    if ( old ) {
	fscanf(sfd, "%hu %hu", &((FPST1 *) fpst)->flags, &((FPST1 *) fpst)->script_lang_index );
	if ( ((FPST1 *) fpst)->script_lang_index>=((SplineFont1 *) sli_sf)->sli_cnt && ((FPST1 *) fpst)->script_lang_index!=SLI_NESTED ) {
	    static int complained=false;
	    if ( ((SplineFont1 *) sli_sf)->sli_cnt==0 )
		IError("'%c%c%c%c' has a script index out of bounds: %d\nYou MUST fix this manually",
			(((FPST1 *) fpst)->tag>>24), (((FPST1 *) fpst)->tag>>16)&0xff, (((FPST1 *) fpst)->tag>>8)&0xff, ((FPST1 *) fpst)->tag&0xff,
			((FPST1 *) fpst)->script_lang_index );
	    else if ( !complained )
		IError("'%c%c%c%c' has a script index out of bounds: %d",
			(((FPST1 *) fpst)->tag>>24), (((FPST1 *) fpst)->tag>>16)&0xff, (((FPST1 *) fpst)->tag>>8)&0xff, ((FPST1 *) fpst)->tag&0xff,
			((FPST1 *) fpst)->script_lang_index );
	    else
		IError("'%c%c%c%c' has a script index out of bounds: %d\n",
			(((FPST1 *) fpst)->tag>>24), (((FPST1 *) fpst)->tag>>16)&0xff, (((FPST1 *) fpst)->tag>>8)&0xff, ((FPST1 *) fpst)->tag&0xff,
			((FPST1 *) fpst)->script_lang_index );
	    if ( ((SplineFont1 *) sli_sf)->sli_cnt!=0 )
		((FPST1 *) fpst)->script_lang_index = ((SplineFont1 *) sli_sf)->sli_cnt-1;
	    complained = true;
	}
	while ( (ch=nlgetc(sfd))==' ' || ch=='\t' );
	if ( ch=='\'' ) {
	    ungetc(ch,sfd);
	    ((FPST1 *) fpst)->tag = gettag(sfd);
	} else
	    ungetc(ch,sfd);
    } else {
	fpst->subtable = SFFindLookupSubtableAndFreeName(sf,SFDReadUTF7Str(sfd));
        if ( !fpst->subtable )
            LogError(_("Missing Subtable definition found in chained context"));
        else
	    fpst->subtable->fpst = fpst;
    }
    fscanf(sfd, "%hu %hu %hu %hu", &fpst->nccnt, &fpst->bccnt, &fpst->fccnt, &fpst->rule_cnt );
    if ( fpst->nccnt!=0 || fpst->bccnt!=0 || fpst->fccnt!=0 ) {
	fpst->nclass = malloc(fpst->nccnt*sizeof(char *));
	fpst->nclassnames = calloc(fpst->nccnt,sizeof(char *));
	if ( fpst->nccnt!=0 ) fpst->nclass[0] = NULL;
	if ( fpst->bccnt!=0 || fpst->fccnt!=0 ) {
	    fpst->bclass = malloc(fpst->bccnt*sizeof(char *));
	    fpst->bclassnames = calloc(fpst->bccnt,sizeof(char *));
	    if (fpst->bccnt!=0 ) fpst->bclass[0] = NULL;
	    fpst->fclass = malloc(fpst->fccnt*sizeof(char *));
	    fpst->fclassnames = calloc(fpst->fccnt,sizeof(char *));
	    if (fpst->fccnt!=0 ) fpst->fclass[0] = NULL;
	}
    }

    for ( j=0; j<3; ++j ) {
	for ( i=1; i<(&fpst->nccnt)[j]; ++i ) {
	    getname(sfd,tok);
	    if ( i==1 && j==0 && strcmp(tok,"Class0:")==0 )
		i=0;
	    getint(sfd,&temp);
	    (&fpst->nclass)[j][i] = malloc(temp+1); (&fpst->nclass)[j][i][temp] = '\0';
	    nlgetc(sfd);	/* skip space */
	    fread((&fpst->nclass)[j][i],1,temp,sfd);
	}
    }

    fpst->rules = calloc(fpst->rule_cnt,sizeof(struct fpst_rule));
    for ( i=0; i<fpst->rule_cnt; ++i ) {
	switch ( fpst->format ) {
	  case pst_glyphs:
	    for ( j=0; j<3; ++j ) {
		getname(sfd,tok);
		getint(sfd,&temp);
		(&fpst->rules[i].u.glyph.names)[j] = malloc(temp+1);
		(&fpst->rules[i].u.glyph.names)[j][temp] = '\0';
		nlgetc(sfd);	/* skip space */
		fread((&fpst->rules[i].u.glyph.names)[j],1,temp,sfd);
	    }
	  break;
	  case pst_class:
	    fscanf( sfd, "%d %d %d", &fpst->rules[i].u.class.ncnt, &fpst->rules[i].u.class.bcnt, &fpst->rules[i].u.class.fcnt );
	    for ( j=0; j<3; ++j ) {
		getname(sfd,tok);
		(&fpst->rules[i].u.class.nclasses)[j] = malloc((&fpst->rules[i].u.class.ncnt)[j]*sizeof(uint16));
		for ( k=0; k<(&fpst->rules[i].u.class.ncnt)[j]; ++k ) {
		    getusint(sfd,&(&fpst->rules[i].u.class.nclasses)[j][k]);
		}
	    }
	  break;
	  case pst_coverage:
	  case pst_reversecoverage:
	    fscanf( sfd, "%d %d %d", &fpst->rules[i].u.coverage.ncnt, &fpst->rules[i].u.coverage.bcnt, &fpst->rules[i].u.coverage.fcnt );
	    for ( j=0; j<3; ++j ) {
		(&fpst->rules[i].u.coverage.ncovers)[j] = malloc((&fpst->rules[i].u.coverage.ncnt)[j]*sizeof(char *));
		for ( k=0; k<(&fpst->rules[i].u.coverage.ncnt)[j]; ++k ) {
		    getname(sfd,tok);
		    getint(sfd,&temp);
		    (&fpst->rules[i].u.coverage.ncovers)[j][k] = malloc(temp+1);
		    (&fpst->rules[i].u.coverage.ncovers)[j][k][temp] = '\0';
		    nlgetc(sfd);	/* skip space */
		    fread((&fpst->rules[i].u.coverage.ncovers)[j][k],1,temp,sfd);
		}
	    }
	  break;
	  default:
	  break;
	}
	switch ( fpst->format ) {
	  case pst_glyphs:
	  case pst_class:
	  case pst_coverage:
	    getint(sfd,&fpst->rules[i].lookup_cnt);
	    fpst->rules[i].lookups = malloc(fpst->rules[i].lookup_cnt*sizeof(struct seqlookup));
	    for ( j=k=0; j<fpst->rules[i].lookup_cnt; ++j ) {
		getname(sfd,tok);
		getint(sfd,&fpst->rules[i].lookups[j].seq);
		fpst->rules[i].lookups[k].lookup = SFD_ParseNestedLookup(sfd,sf,old);
		if ( fpst->rules[i].lookups[k].lookup!=NULL )
		    ++k;
	    }
	    fpst->rules[i].lookup_cnt = k;
	  break;
	  case pst_reversecoverage:
	    getname(sfd,tok);
	    getint(sfd,&temp);
	    fpst->rules[i].u.rcoverage.replacements = malloc(temp+1);
	    fpst->rules[i].u.rcoverage.replacements[temp] = '\0';
	    nlgetc(sfd);	/* skip space */
	    fread(fpst->rules[i].u.rcoverage.replacements,1,temp,sfd);
	  break;
	  default:
	  break;
	}
    }
    getname(sfd,tok);	/* EndFPST, or one of the ClassName tokens (in newer sfds) */
    while ( strcmp(tok,"ClassNames:")==0 || strcmp(tok,"BClassNames:")==0 ||
	    strcmp(tok,"FClassNames:")==0 ) {
	int which = strcmp(tok,"ClassNames:")==0 ? 0 :
		    strcmp(tok,"BClassNames:")==0 ? 1 : 2;
	int cnt = (&fpst->nccnt)[which];
	char **classnames = (&fpst->nclassnames)[which];
	int i;

	for ( i=0; i<cnt; ++i )
	    classnames[i] = SFDReadUTF7Str(sfd);
	getname(sfd,tok);	/* EndFPST, or one of the ClassName tokens (in newer sfds) */
    }

}


static int SFD_Dump( FILE *sfd, SplineFont *sf, EncMap *map, EncMap *normal,
		     int todir, char *dirname)
{
    int i, realcnt;
    BDFFont *bdf;
    int *newgids = NULL;
    int err = false;

    if ( normal!=NULL )
	map = normal;

    SFD_DumpSplineFontMetadata( sfd, sf ); //, map, normal, todir, dirname );

    if ( sf->MATH!=NULL ) {
	struct MATH *math = sf->MATH;
	for ( i=0; math_constants_descriptor[i].script_name!=NULL; ++i ) {
	    fprintf( sfd, "MATH:%s: %d", math_constants_descriptor[i].script_name,
		    *((int16 *) (((char *) (math)) + math_constants_descriptor[i].offset)) );
	    if ( math_constants_descriptor[i].devtab_offset>=0 ) {
		DeviceTable **devtab = (DeviceTable **) (((char *) (math)) + math_constants_descriptor[i].devtab_offset );
		putc(' ',sfd);
		SFDDumpDeviceTable(sfd,*devtab);
	    }
	    putc('\n',sfd);
	}
    }
    if ( sf->python_persistent!=NULL )
	SFDPickleMe(sfd,sf->python_persistent, sf->python_persistent_has_lists);
    if ( sf->subfontcnt!=0 ) {
	/* CID fonts have no encodings, they have registry info instead */
	fprintf(sfd, "Registry: %s\n", sf->cidregistry );
	fprintf(sfd, "Ordering: %s\n", sf->ordering );
	fprintf(sfd, "Supplement: %d\n", sf->supplement );
	fprintf(sfd, "CIDVersion: %g\n", (double) sf->cidversion );	/* This is a number whereas "version" is a string */
    } else
	SFDDumpEncoding(sfd,map->enc,"Encoding");
    if ( normal!=NULL )
	fprintf(sfd, "Compacted: 1\n" );
    fprintf( sfd, "UnicodeInterp: %s\n", unicode_interp_names[sf->uni_interp]);
    fprintf( sfd, "NameList: %s\n", sf->for_new_glyphs->title );

    if ( map->remap!=NULL ) {
	struct remap *remap;
	int n;
	for ( n=0,remap = map->remap; remap->infont!=-1; ++n, ++remap );
	fprintf( sfd, "RemapN: %d\n", n );
	for ( remap = map->remap; remap->infont!=-1; ++remap )
	    fprintf(sfd, "Remap: %x %x %d\n", (int) remap->firstenc, (int) remap->lastenc, (int) remap->infont );
    }
    if ( sf->display_size!=0 )
	fprintf( sfd, "DisplaySize: %d\n", sf->display_size );
    if ( sf->display_layer!=ly_fore )
	fprintf( sfd, "DisplayLayer: %d\n", sf->display_layer );
    fprintf( sfd, "AntiAlias: %d\n", sf->display_antialias );
    fprintf( sfd, "FitToEm: %d\n", sf->display_bbsized );
    if ( sf->extrema_bound!=0 )
	fprintf( sfd, "ExtremaBound: %d\n", sf->extrema_bound );
    if ( sf->width_separation!=0 )
	fprintf( sfd, "WidthSeparation: %d\n", sf->width_separation );
    {
	int rc, cc, te;
	if ( (te = FVWinInfo(sf->fv,&cc,&rc))!= -1 )
	    fprintf( sfd, "WinInfo: %d %d %d\n", te, cc, rc );
	else if ( sf->top_enc!=-1 )
	    fprintf( sfd, "WinInfo: %d %d %d\n", sf->top_enc, sf->desired_col_cnt,
		sf->desired_row_cnt);
    }
    if ( sf->onlybitmaps!=0 )
	fprintf( sfd, "OnlyBitmaps: %d\n", sf->onlybitmaps );
    if ( sf->private!=NULL )
	SFDDumpPrivate(sfd,sf->private);
#if HANYANG
    if ( sf->rules!=NULL )
	SFDDumpCompositionRules(sfd,sf->rules);
#endif
    if ( sf->grid.splines!=NULL ) {
	if ( sf->grid.order2 )
	    fprintf(sfd, "GridOrder2: %d\n", sf->grid.order2 );
	fprintf(sfd, "Grid\n" );
	SFDDumpSplineSet(sfd,sf->grid.splines,sf->grid.order2);
    }
    if ( sf->texdata.type!=tex_unset ) {
	fprintf(sfd, "TeXData: %d %d", (int) sf->texdata.type, (int) ((sf->design_size<<19)+2)/5 );
	for ( i=0; i<22; ++i )
	    fprintf(sfd, " %d", (int) sf->texdata.params[i]);
	putc('\n',sfd);
    }
    if ( sf->anchor!=NULL ) {
	AnchorClass *an;
	fprintf(sfd, "AnchorClass2:");
	for ( an=sf->anchor; an!=NULL; an=an->next ) {
	    putc(' ',sfd);
	    SFDDumpUTF7Str(sfd,an->name);
            if ( an->subtable!=NULL ) {
	        putc(' ',sfd);
	        SFDDumpUTF7Str(sfd,an->subtable->subtable_name);
            }
            else
                fprintf(sfd, "\"\" ");
	}
	putc('\n',sfd);
    }

    if ( sf->subfontcnt!=0 ) {
	if ( todir ) {
	    for ( i=0; i<sf->subfontcnt; ++i ) {
		char *subfont = malloc(strlen(dirname)+1+strlen(sf->subfonts[i]->fontname)+20);
		char *fontprops;
		FILE *ssfd;
		sprintf( subfont,"%s/%s" SUBFONT_EXT, dirname, sf->subfonts[i]->fontname );
		GFileMkDir(subfont, 0755);
		fontprops = malloc(strlen(subfont)+strlen("/" FONT_PROPS)+1);
		strcpy(fontprops,subfont); strcat(fontprops,"/" FONT_PROPS);
		ssfd = fopen( fontprops,"w");
		if ( ssfd!=NULL ) {
		    err |= SFD_Dump(ssfd,sf->subfonts[i],map,NULL,todir,subfont);
		    if ( ferror(ssfd) ) err = true;
		    if ( fclose(ssfd)) err = true;
		} else
		    err = true;
		free(fontprops);
		free(subfont);
	    }
	} else {
	    int max;
	    for ( i=max=0; i<sf->subfontcnt; ++i )
		if ( max<sf->subfonts[i]->glyphcnt )
		    max = sf->subfonts[i]->glyphcnt;
	    fprintf(sfd, "BeginSubFonts: %d %d\n", sf->subfontcnt, max );
	    for ( i=0; i<sf->subfontcnt; ++i )
		SFD_Dump(sfd,sf->subfonts[i],map,NULL,false, NULL);
	    fprintf(sfd, "EndSubFonts\n" );
	}
    } else {
	int enccount = map->enccount;
	if ( sf->cidmaster!=NULL ) {
	    realcnt = -1;
	    enccount = sf->glyphcnt;
	} else {
	    realcnt = 0;
	    for ( i=0; i<sf->glyphcnt; ++i ) if ( !SFDOmit(sf->glyphs[i]) )
		++realcnt;
	    if ( realcnt!=sf->glyphcnt ) {
		newgids = malloc(sf->glyphcnt*sizeof(int));
		realcnt = 0;
		for ( i=0; i<sf->glyphcnt; ++i )
		    if ( SFDOmit(sf->glyphs[i]) )
			newgids[i] = -1;
		    else
			newgids[i] = realcnt++;
	    }
	}
	if ( !todir )
	    fprintf(sfd, "BeginChars: %d %d\n",
	        enccount<map->enc->char_cnt? map->enc->char_cnt : enccount,
	        realcnt );
	for ( i=0; i<sf->glyphcnt; ++i ) {
	    if ( !SFDOmit(sf->glyphs[i]) ) {
		if ( !todir )
		SFDDumpChar(sfd,sf->glyphs[i],map,newgids,todir,1);
		else {
		    char *glyphfile = malloc(strlen(dirname)+2*strlen(sf->glyphs[i]->name)+20);
		    FILE *gsfd;
		    appendnames(glyphfile,dirname,"/",sf->glyphs[i]->name,GLYPH_EXT );
		    gsfd = fopen(glyphfile,"w");
		    if ( gsfd!=NULL ) {
			SFDDumpChar(gsfd,sf->glyphs[i],map,newgids,todir,1);
			if ( ferror(gsfd)) err = true;
			if ( fclose(gsfd)) err = true;
		    } else
			err = true;
		    free(glyphfile);
		}
	    }
	    ff_progress_next();
	}
	if ( !todir )
	    fprintf(sfd, "EndChars\n" );
    }

    if ( sf->bitmaps!=NULL )
	ff_progress_change_line2(_("Saving Bitmaps"));
    for ( bdf = sf->bitmaps; bdf!=NULL; bdf=bdf->next ) {
	if ( todir ) {
	    char *strike = malloc(strlen(dirname)+1+20+20);
	    char *strikeprops;
	    FILE *ssfd;
	    sprintf( strike,"%s/%d" STRIKE_EXT, dirname, bdf->pixelsize );
	    GFileMkDir(strike, 0755);
	    strikeprops = malloc(strlen(strike)+strlen("/" STRIKE_PROPS)+1);
	    strcpy(strikeprops,strike); strcat(strikeprops,"/" STRIKE_PROPS);
	    ssfd = fopen( strikeprops,"w");
	    if ( ssfd!=NULL ) {
		err |= SFDDumpBitmapFont(ssfd,bdf,map,newgids,todir,strike);
		if ( ferror(ssfd) ) err = true;
		if ( fclose(ssfd)) err = true;
	    } else
		err = true;
	    free(strikeprops);
	    free(strike);
	} else
	    SFDDumpBitmapFont(sfd,bdf,map,newgids,todir,dirname);
    }
    fprintf(sfd, sf->cidmaster==NULL?"EndSplineFont\n":"EndSubSplineFont\n" );
    free(newgids);
return( err );
}


static void SFDDumpTtfInstrsExplicit(FILE *sfd,uint8 *ttf_instrs, int16 ttf_instrs_len )
{
    char *instrs = _IVUnParseInstrs( ttf_instrs, ttf_instrs_len );
    char *pt;
    fprintf( sfd, "TtInstrs:\n" );
    for ( pt=instrs; *pt!='\0'; ++pt )
	putc(*pt,sfd);
    if ( pt[-1]!='\n' )
	putc('\n',sfd);
    free(instrs);
    fprintf( sfd, "%s\n", end_tt_instrs );
}


static int getreal(FILE *sfd, real *val) {
    char tokbuf[100];
    int ch;
    char *pt=tokbuf, *end = tokbuf+100-2, *nend;

    while ( isspace(ch = nlgetc(sfd)));
    if ( ch!='e' && ch!='E' )		/* real's can't begin with exponants */
	while ( isdigit(ch) || ch=='-' || ch=='+' || ch=='e' || ch=='E' || ch=='.' || ch==',' ) {
	    if ( pt<end ) *pt++ = ch;
	    ch = nlgetc(sfd);
	}
    *pt='\0';
    ungetc(ch,sfd);
    *val = strtod(tokbuf,&nend);
    /* Beware of different locals! */
    if ( *nend!='\0' ) {
	if ( *nend=='.' )
	    *nend = ',';
	else if ( *nend==',' )
	    *nend = '.';
	*val = strtod(tokbuf,&nend);
    }
return( pt!=tokbuf && *nend=='\0'?1:ch==EOF?-1: 0 );
}


static void SFDDumpGradient(FILE *sfd, const char *keyword, struct gradient *gradient) {
    int i;

    /* Use ";" as a coord separator because we treat "," as a potential decimal point */
    fprintf( sfd, "%s %g;%g %g;%g %g %s %d ", keyword,
	    (double) gradient->start.x, (double) gradient->start.y,
	    (double) gradient->stop.x, (double) gradient->stop.y,
	    (double) gradient->radius,
	    spreads[gradient->sm],
	    gradient->stop_cnt );
    for ( i=0 ; i<gradient->stop_cnt; ++i ) {
	fprintf( sfd, "{%g #%06x %g} ", (double) gradient->grad_stops[i].offset,
		gradient->grad_stops[i].col, (double) gradient->grad_stops[i].opacity );
    }
    putc('\n',sfd);
}


static struct ttf_table *SFDGetTtTable(FILE *sfd, SplineFont *sf,struct ttf_table *lasttab[2]) {
    /* We've read the TtTable token, it is followed by a tag */
    /* and then the instructions in text format */
    int ch;
    int which;
    struct ttf_table *tab = chunkalloc(sizeof(struct ttf_table));
    char *buf=NULL, *pt=buf, *end=buf;
    int backlen = strlen(end_tt_instrs);

    tab->tag = gettag(sfd);

    if ( tab->tag==CHR('f','p','g','m') || tab->tag==CHR('p','r','e','p') ||
	    tab->tag==CHR('c','v','t',' ') || tab->tag==CHR('m','a','x','p'))
	which = 0;
    else
	which = 1;

    while ( (ch=nlgetc(sfd))!=EOF ) {
	if ( pt>=end ) {
	    char *newbuf = realloc(buf,(end-buf+200));
	    pt = newbuf+(pt-buf);
	    end = newbuf+(end+200-buf);
	    buf = newbuf;
	}
	*pt++ = ch;
	if ( pt-buf>backlen && strncmp(pt-backlen,end_tt_instrs,backlen)==0 ) {
	    pt -= backlen;
    break;
	}
    }
    *pt = '\0';
    tab->data = _IVParse(sf,buf,&tab->len,tterr,NULL);
    free(buf);

    if ( lasttab[which]!=NULL )
	lasttab[which]->next = tab;
    else if ( which==0 )
	sf->ttf_tables = tab;
    else
	sf->ttf_tab_saved = tab;
    lasttab[which] = tab;
return( tab );
}


static DStemInfo *SFDReadDHints( SplineFont *sf,FILE *sfd,int old ) {
    DStemInfo *head=NULL, *last=NULL, *cur;
    int i;
    BasePoint bp[4], *bpref[4], left, right, unit;
    double rstartoff, rendoff, lendoff;

    if ( old ) {
        for ( i=0 ; i<4 ; i++ ) bpref[i] = &bp[i];

        while ( getreal( sfd,&bp[0].x ) && getreal( sfd,&bp[0].y ) &&
	        getreal( sfd,&bp[1].x ) && getreal( sfd,&bp[1].y ) &&
	        getreal( sfd,&bp[2].x ) && getreal( sfd,&bp[2].y ) &&
	        getreal( sfd,&bp[3].x ) && getreal( sfd,&bp[3].y )) {

            /* Ensure point coordinates specified in the sfd file do */
            /* form a diagonal line */
            if ( PointsDiagonalable( sf,bpref,&unit )) {
	        cur = chunkalloc( sizeof( DStemInfo ));
	        cur->left = *bpref[0];
	        cur->right = *bpref[1];
                cur->unit = unit;
                /* Generate a temporary hint instance, so that the hint can */
                /* be visible in charview even if subsequent rebuilding instances */
                /* fails (e. g. for composite characters) */
                cur->where = chunkalloc( sizeof( HintInstance ));
                rstartoff = ( cur->right.x - cur->left.x ) * cur->unit.x +
                            ( cur->right.y - cur->left.y ) * cur->unit.y;
                rendoff =   ( bpref[2]->x - cur->left.x ) * cur->unit.x +
                            ( bpref[2]->y - cur->left.y ) * cur->unit.y;
                lendoff =   ( bpref[3]->x - cur->left.x ) * cur->unit.x +
                            ( bpref[3]->y - cur->left.y ) * cur->unit.y;
                cur->where->begin = ( rstartoff > 0 ) ? rstartoff : 0;
                cur->where->end   = ( rendoff > lendoff ) ? lendoff : rendoff;
                MergeDStemInfo( sf,&head,cur );
            }
        }
    } else {
        while ( getreal( sfd,&left.x ) && getreal( sfd,&left.y ) &&
                getreal( sfd,&right.x ) && getreal( sfd,&right.y ) &&
                getreal( sfd,&unit.x ) && getreal( sfd,&unit.y )) {
	    cur = chunkalloc( sizeof( DStemInfo ));
	    cur->left = left;
	    cur->right = right;
            cur->unit = unit;
	    cur->where = SFDReadHintInstances( sfd,NULL );
	    if ( head == NULL )
	        head = cur;
	    else
	        last->next = cur;
	    last = cur;
        }
    }
return( head );
}


static void SFDSetEncMap(SplineFont *sf,int orig_pos,int enc) {
    EncMap *map = sf->map;

    if ( map==NULL )
return;

    if ( orig_pos>=map->backmax ) {
	int old = map->backmax;
	map->backmax = orig_pos+10;
	map->backmap = realloc(map->backmap,map->backmax*sizeof(int));
	memset(map->backmap+old,-1,(map->backmax-old)*sizeof(int));
    }
    if ( map->backmap[orig_pos] == -1 )		/* backmap will not be unique if multiple encodings come from same glyph */
	map->backmap[orig_pos] = enc;
    if ( enc>=map->encmax ) {
	int old = map->encmax;
	map->encmax = enc+10;
	map->map = realloc(map->map,map->encmax*sizeof(int));
	memset(map->map+old,-1,(map->encmax-old)*sizeof(int));
    }
    if ( enc>=map->enccount )
	map->enccount = enc+1;
    if ( enc!=-1 )
	map->map[enc] = orig_pos;
}


static void SFDDumpAnchorPoints(FILE *sfd,AnchorPoint *ap) {
    if (ap==NULL) {
	return;
    }

    for ( ; ap!=NULL; ap=ap->next )
    {
	fprintf( sfd, "AnchorPoint: " );
	SFDDumpUTF7Str(sfd,ap->anchor->name);
	putc(' ',sfd);
	fprintf( sfd, "%g %g %s %d",
		(double) ap->me.x, (double) ap->me.y,
		ap->type==at_centry ? "entry" :
		ap->type==at_cexit ? "exit" :
		ap->type==at_mark ? "mark" :
		ap->type==at_basechar ? "basechar" :
		ap->type==at_baselig ? "baselig" : "basemark",
		ap->lig_index );
	if ( ap->xadjust.corrections!=NULL || ap->yadjust.corrections!=NULL ) {
	    putc(' ',sfd);
	    SFDDumpDeviceTable(sfd,&ap->xadjust);
	    putc(' ',sfd);
	    SFDDumpDeviceTable(sfd,&ap->yadjust);
	} else
	if ( ap->has_ttf_pt )
	    fprintf( sfd, " %d", ap->ttf_pt_index );
	putc('\n',sfd);
    }
}


static ImageList *SFDGetImagePNG(FILE *sfd) {
    int pnglen;
    ImageList *img;
    struct enc85 dec = {0};
    int i, ch;

    img = calloc(1,sizeof(ImageList));
    dec.pos = -1;
    dec.sfd = sfd;

    getint(sfd,&pnglen);
    getreal(sfd,&img->xoff);
    getreal(sfd,&img->yoff);
    getreal(sfd,&img->xscale);
    getreal(sfd,&img->yscale);

    while ( (ch=nlgetc(sfd))==' ' || ch=='\t' )
        /* skip */;

    char* pngbuf = malloc(pnglen * sizeof(char));
    if (pngbuf == NULL) {
        IError("Failed to allocate buffer to read PNG in SFD file");
        return NULL;
    }

    for (i = 0; i<pnglen; ++i) {
        pngbuf[i] = Dec85(&dec);
    }

    img->image = GImageReadPngBuf(pngbuf, pnglen);
    free(pngbuf);

    if (img->image == NULL) {
        IError("Failed to read PNG in SFD file, skipping it.");
        free(img);
        return NULL;
    }

    img->bb.minx = img->xoff; img->bb.maxy = img->yoff;
    img->bb.maxx = img->xoff + GImageGetWidth(img->image)*img->xscale;
    img->bb.miny = img->yoff - GImageGetHeight(img->image)*img->yscale;
    return img;
}


void SFDGetPSTs( FILE *sfd, SplineChar *sc, char* ttok ) {
    struct splinefont * sf = sc->parent;
    char tok[2001], ch;
    int isliga = 0, ispos, issubs, ismult, islcar, ispair, temp;
    PST *last = NULL;
    uint32 script = 0;
    SplineFont *sli_sf = sf->cidmaster ? sf->cidmaster : sf;

    strncpy(tok,ttok,sizeof(tok)-1);

    if ( strmatch(tok,"Script:")==0 ) {
	/* Obsolete. But still used for parsing obsolete ligature/subs tags */
	while ( (ch=nlgetc(sfd))==' ' || ch=='\t' );
	if ( ch=='\n' || ch=='\r' )
	    script = 0;
	else {
	    ungetc(ch,sfd);
	    script = gettag(sfd);
	}
    } else if ( (ispos = (strmatch(tok,"Position:")==0)) ||
		( ispos  = (strmatch(tok,"Position2:")==0)) ||
		( ispair = (strmatch(tok,"PairPos:")==0)) ||
		( ispair = (strmatch(tok,"PairPos2:")==0)) ||
		( islcar = (strmatch(tok,"LCarets:")==0)) ||
		( islcar = (strmatch(tok,"LCarets2:")==0)) ||
		( isliga = (strmatch(tok,"Ligature:")==0)) ||
		( isliga = (strmatch(tok,"Ligature2:")==0)) ||
		( issubs = (strmatch(tok,"Substitution:")==0)) ||
		( issubs = (strmatch(tok,"Substitution2:")==0)) ||
		( ismult = (strmatch(tok,"MultipleSubs:")==0)) ||
		( ismult = (strmatch(tok,"MultipleSubs2:")==0)) ||
		strmatch(tok,"AlternateSubs:")==0 ||
		strmatch(tok,"AlternateSubs2:")==0 ) {
	    PST *pst;
	    int old, type;
	    type = ispos ? pst_position :
			 ispair ? pst_pair :
			 islcar ? pst_lcaret :
			 isliga ? pst_ligature :
			 issubs ? pst_substitution :
			 ismult ? pst_multiple :
			 pst_alternate;
	    if ( strchr(tok,'2')!=NULL ) {
		old = false;
		pst = chunkalloc(sizeof(PST));
		if ( type!=pst_lcaret )
		    pst->subtable = SFFindLookupSubtableAndFreeName(sf,SFDReadUTF7Str(sfd));
	    } else {
		old = true;
		pst = chunkalloc(sizeof(PST1));
		((PST1 *) pst)->tag = CHR('l','i','g','a');
		((PST1 *) pst)->script_lang_index = 0xffff;
		while ( (ch=nlgetc(sfd))==' ' || ch=='\t' );
		if ( isdigit(ch)) {
		    int temp;
		    ungetc(ch,sfd);
		    getint(sfd,&temp);
		    ((PST1 *) pst)->flags = temp;
		    while ( (ch=nlgetc(sfd))==' ' || ch=='\t' );
		} else
		    ((PST1 *) pst)->flags = 0 /*PSTDefaultFlags(type,sc)*/;
		if ( isdigit(ch)) {
		    ungetc(ch,sfd);
		    getusint(sfd,&((PST1 *) pst)->script_lang_index);
		    while ( (ch=nlgetc(sfd))==' ' || ch=='\t' );
		} else
		    ((PST1 *) pst)->script_lang_index = SFFindBiggestScriptLangIndex(sf,
			    script!=0?script:SCScriptFromUnicode(sc),DEFAULT_LANG);
		if ( ch=='\'' ) {
		    ungetc(ch,sfd);
		    ((PST1 *) pst)->tag = gettag(sfd);
		} else if ( ch=='<' ) {
		    getint(sfd,&temp);
		    ((PST1 *) pst)->tag = temp<<16;
		    nlgetc(sfd);	/* comma */
		    getint(sfd,&temp);
		    ((PST1 *) pst)->tag |= temp;
		    nlgetc(sfd);	/* close '>' */
		    ((PST1 *) pst)->macfeature = true;
		} else
		    ungetc(ch,sfd);
		if ( type==pst_lcaret ) {
		/* These are meaningless for lcarets, set them to innocuous values */
		    ((PST1 *) pst)->script_lang_index = SLI_UNKNOWN;
		    ((PST1 *) pst)->tag = CHR(' ',' ',' ',' ');
		} else if ( ((PST1 *) pst)->script_lang_index>=((SplineFont1 *) sli_sf)->sli_cnt && ((PST1 *) pst)->script_lang_index!=SLI_NESTED ) {
		    static int complained=false;
		    if ( !complained )
			IError("'%c%c%c%c' in %s has a script index out of bounds: %d",
				(((PST1 *) pst)->tag>>24), (((PST1 *) pst)->tag>>16)&0xff, (((PST1 *) pst)->tag>>8)&0xff, ((PST1 *) pst)->tag&0xff,
				sc->name, ((PST1 *) pst)->script_lang_index );
		    else
			IError( "'%c%c%c%c' in %s has a script index out of bounds: %d\n",
				(((PST1 *) pst)->tag>>24), (((PST1 *) pst)->tag>>16)&0xff, (((PST1 *) pst)->tag>>8)&0xff, ((PST1 *) pst)->tag&0xff,
				sc->name, ((PST1 *) pst)->script_lang_index );
		    ((PST1 *) pst)->script_lang_index = SFFindBiggestScriptLangIndex(sli_sf,
			    SCScriptFromUnicode(sc),DEFAULT_LANG);
		    complained = true;
		}
	    }
	    if ( (sf->sfd_version<2)!=old ) {
		IError( "Version mixup in PST of sfd file." );
exit(1);
	    }
	    if ( last==NULL )
		sc->possub = pst;
	    else
		last->next = pst;
	    last = pst;
	    pst->type = type;
	    if ( pst->type==pst_position ) {
		fscanf( sfd, " dx=%hd dy=%hd dh=%hd dv=%hd",
			&pst->u.pos.xoff, &pst->u.pos.yoff,
			&pst->u.pos.h_adv_off, &pst->u.pos.v_adv_off);
		pst->u.pos.adjust = SFDReadValDevTab(sfd);
		ch = nlgetc(sfd);		/* Eat new line */
	    } else if ( pst->type==pst_pair ) {
		getname(sfd,tok);
		pst->u.pair.paired = copy(tok);
		pst->u.pair.vr = chunkalloc(sizeof(struct vr [2]));
		fscanf( sfd, " dx=%hd dy=%hd dh=%hd dv=%hd",
			&pst->u.pair.vr[0].xoff, &pst->u.pair.vr[0].yoff,
			&pst->u.pair.vr[0].h_adv_off, &pst->u.pair.vr[0].v_adv_off);
		pst->u.pair.vr[0].adjust = SFDReadValDevTab(sfd);
		fscanf( sfd, " dx=%hd dy=%hd dh=%hd dv=%hd",
			&pst->u.pair.vr[1].xoff, &pst->u.pair.vr[1].yoff,
			&pst->u.pair.vr[1].h_adv_off, &pst->u.pair.vr[1].v_adv_off);
		pst->u.pair.vr[0].adjust = SFDReadValDevTab(sfd);
		ch = nlgetc(sfd);
	    } else if ( pst->type==pst_lcaret ) {
		int i;
		fscanf( sfd, " %d", &pst->u.lcaret.cnt );
		pst->u.lcaret.carets = malloc(pst->u.lcaret.cnt*sizeof(int16));
		for ( i=0; i<pst->u.lcaret.cnt; ++i )
		    fscanf( sfd, " %hd", &pst->u.lcaret.carets[i]);
		geteol(sfd,tok);
	    } else {
		geteol(sfd,tok);
		pst->u.lig.components = copy(tok);	/* it's in the same place for all formats */
		if ( isliga ) {
		    pst->u.lig.lig = sc;
		    if ( old )
			last = (PST *) LigaCreateFromOldStyleMultiple((PST1 *) pst);
		}
	    }
#ifdef FONTFORGE_CONFIG_CVT_OLD_MAC_FEATURES
	    if ( old )
		CvtOldMacFeature((PST1 *) pst);
#endif
    } else {
	return;
    }

    // we matched something, grab the next top level token to ttok
    getname( sfd, ttok );
}


static void SFDDumpJustify(FILE *sfd,SplineFont *sf) {
    Justify *jscript;
    struct jstf_lang *jlang;
    int i;

    for ( jscript = sf->justify; jscript!=NULL; jscript=jscript->next ) {
	fprintf( sfd, "Justify: '%c%c%c%c'\n",
		jscript->script>>24,
		jscript->script>>16,
		jscript->script>>8,
		jscript->script);
	if ( jscript->extenders!=NULL )
	    fprintf( sfd, "JstfExtender: %s\n", jscript->extenders );
	for ( jlang = jscript->langs; jlang!=NULL; jlang = jlang->next ) {
	    fprintf( sfd, "JstfLang: '%c%c%c%c' %d\n",
		jlang->lang>>24,
		jlang->lang>>16,
		jlang->lang>>8,
		jlang->lang, jlang->cnt );
	    for ( i=0; i<jlang->cnt; ++i ) {
		fprintf( sfd, "JstfPrio:\n" );
		SFDDumpJSTFLookups(sfd,"JstfEnableShrink:", jlang->prios[i].enableShrink );
		SFDDumpJSTFLookups(sfd,"JstfDisableShrink:", jlang->prios[i].disableShrink );
		SFDDumpJSTFLookups(sfd,"JstfMaxShrink:", jlang->prios[i].maxShrink );
		SFDDumpJSTFLookups(sfd,"JstfEnableExtend:", jlang->prios[i].enableExtend );
		SFDDumpJSTFLookups(sfd,"JstfDisableExtend:", jlang->prios[i].disableExtend );
		SFDDumpJSTFLookups(sfd,"JstfMaxExtend:", jlang->prios[i].maxExtend );
	    }
	}
    }
    if ( sf->justify!=NULL )
	fprintf( sfd, "EndJustify\n" );
}


static void SFDGetSubrs(FILE *sfd) {
    /* Obselete, parse it in case there are any old sfds */
    int i, cnt, tot, len;
    struct enc85 dec;

    getint(sfd,&cnt);
    tot = 0;
    for ( i=0; i<cnt; ++i ) {
	getint(sfd,&len);
	tot += len;
    }

    memset(&dec,'\0', sizeof(dec)); dec.pos = -1;
    dec.sfd = sfd;
    for ( i=0; i<tot; ++i )
	Dec85(&dec);
}


SplineFont *SFRecoverFile(char *autosavename,int inquire,int *state) {
    FILE *asfd;
    SplineFont *ret;
    char tok[1025];

    if (!inquire) {
        *state = 1; //Default to recover all
    }
    if (!ask_about_file(autosavename, state, &asfd)) {
return( NULL );
    }
    locale_t tmplocale; locale_t oldlocale; // Declare temporary locale storage.
    switch_to_c_locale(&tmplocale, &oldlocale); // Switch to the C locale temporarily and cache the old locale.
    ret = SlurpRecovery(asfd,tok,sizeof(tok));
    if ( ret==NULL ) {
	const char *buts[3];
	buts[0] = "_Forget It"; buts[1] = "_Try Again"; buts[2] = NULL;
	if ( ff_ask(_("Recovery Failed"),(const char **) buts,0,1,_("Automagic recovery of changes to %.80s failed.\nShould FontForge try again to recover next time you start it?"),tok)==0 )
	    unlink(autosavename);
    }
    switch_to_old_locale(&tmplocale, &oldlocale); // Switch to the cached locale.
    fclose(asfd);
    if ( ret )
	ret->autosavename = copy(autosavename);
return( ret );
}


static void SFDCleanupAnchorClasses(SplineFont *sf) {
    AnchorClass *ac;
    AnchorPoint *ap;
    int i, j, scnt;
#define S_MAX	100
    uint32 scripts[S_MAX];
    int merge=0;

    for ( ac = sf->anchor; ac!=NULL; ac=ac->next ) {
	if ( ((AnchorClass1 *) ac)->script_lang_index==0xffff ) {
	    scnt = 0;
	    for ( i=0; i<sf->glyphcnt; ++i ) if ( sf->glyphs[i]!=NULL ) {
		for ( ap = sf->glyphs[i]->anchor; ap!=NULL && ap->anchor!=ac; ap=ap->next );
		if ( ap!=NULL && scnt<S_MAX ) {
		    uint32 script = SCScriptFromUnicode(sf->glyphs[i]);
		    if ( script==0 )
	    continue;
		    for ( j=0; j<scnt; ++j )
			if ( scripts[j]==script )
		    break;
		    if ( j==scnt )
			scripts[scnt++] = script;
		}
	    }
	    ((AnchorClass1 *) ac)->script_lang_index = SFAddScriptIndex((SplineFont1 *) sf,scripts,scnt);
	}
	if ( ((AnchorClass1 *) ac)->merge_with == 0xffff )
	    ((AnchorClass1 *) ac)->merge_with = ++merge;
    }
#undef S_MAX
}


static void SFDDumpImage(FILE *sfd,ImageList *img) {
    GImage *image = img->image;
    struct _GImage *base = image->list_len==0?image->u.image:image->u.images[0];
    struct enc85 enc;
    int rlelen;
    uint8 *rle;
    int i;

    rle = image2rle(base,&rlelen);
    fprintf(sfd, "Image: %d %d %d %d %d %x %g %g %g %g %d\n",
	    (int) base->width, (int) base->height, base->image_type,
	    (int) (base->image_type==it_true?3*base->width:base->bytes_per_line),
	    base->clut==NULL?0:base->clut->clut_len,(int) base->trans,
	    (double) img->xoff, (double) img->yoff, (double) img->xscale, (double) img->yscale, rlelen );
    memset(&enc,'\0',sizeof(enc));
    enc.sfd = sfd;
    if ( base->clut!=NULL ) {
	for ( i=0; i<base->clut->clut_len; ++i ) {
	    SFDEnc85(&enc,base->clut->clut[i]>>16);
	    SFDEnc85(&enc,(base->clut->clut[i]>>8)&0xff);
	    SFDEnc85(&enc,base->clut->clut[i]&0xff);
	}
    }
    if ( rle!=NULL ) {
	uint8 *pt=rle, *end=rle+rlelen;
	while ( pt<end )
	    SFDEnc85(&enc,*pt++);
	free( rle );
    } else {
	for ( i=0; i<base->height; ++i ) {
	    if ( base->image_type==it_rgba ) {
		uint32 *ipt = (uint32 *) (base->data + i*base->bytes_per_line);
		uint32 *iend = (uint32 *) (base->data + (i+1)*base->bytes_per_line);
		while ( ipt<iend ) {
		    SFDEnc85(&enc,*ipt>>24);
		    SFDEnc85(&enc,(*ipt>>16)&0xff);
		    SFDEnc85(&enc,(*ipt>>8)&0xff);
		    SFDEnc85(&enc,*ipt&0xff);
		    ++ipt;
		}
	    } else if ( base->image_type==it_true ) {
		int *ipt = (int *) (base->data + i*base->bytes_per_line);
		int *iend = (int *) (base->data + (i+1)*base->bytes_per_line);
		while ( ipt<iend ) {
		    SFDEnc85(&enc,*ipt>>16);
		    SFDEnc85(&enc,(*ipt>>8)&0xff);
		    SFDEnc85(&enc,*ipt&0xff);
		    ++ipt;
		}
	    } else {
		uint8 *pt = (uint8 *) (base->data + i*base->bytes_per_line);
		uint8 *end = (uint8 *) (base->data + (i+1)*base->bytes_per_line);
		while ( pt<end ) {
		    SFDEnc85(&enc,*pt);
		    ++pt;
		}
	    }
	}
    }
    SFDEnc85EndEnc(&enc);
    fprintf(sfd,"\nEndImage\n" );
}


static void tterr(void *UNUSED(rubbish), char *message, int UNUSED(pos)) {
    LogError(_("When loading tt instrs from sfd: %s\n"), message );
}


static void SFDGetHintMask(FILE *sfd,HintMask *hintmask) {
    int nibble = 0, ch;

    memset(hintmask,0,sizeof(HintMask));
    for (;;) {
	ch = nlgetc(sfd);
	if ( isdigit(ch))
	    ch -= '0';
	else if ( ch>='a' && ch<='f' )
	    ch -= 'a'-10;
	else if ( ch>='A' && ch<='F' )
	    ch -= 'A'-10;
	else {
	    ungetc(ch,sfd);
    break;
	}
	if ( nibble<2*HntMax/8 )
	    (*hintmask)[nibble>>1] |= ch<<(4*(1-(nibble&1)));
	++nibble;
    }
}


static unsigned int read_le32(const unsigned char *p)
{
    return ((unsigned int) p[0])
        | ((unsigned int) p[1] << 8)
        | ((unsigned int) p[2] << 16)
        | ((unsigned int) p[3] << 24);
}

gzip: fix compression size calculation (oss-fuzz 27261)

Signed-off-by: davkor <david@adalogics.com>


int flb_gzip_compress(void *in_data, size_t in_len,
                      void **out_data, size_t *out_len)
{
    int flush;
    int status;
    int footer_start;
    uint8_t *pb;
    size_t out_size;
    void *out_buf;
    z_stream strm;
    mz_ulong crc;

    /*
     * GZIP relies on an algorithm with worst-case expansion
     * of 5 bytes per 32KB data. This means we need to create a variable
     * length output, that depends on the input length.
     * See RFC 1951 for details.
     */
    int max_input_expansion = ((int)(in_len / 32000) + 1) * 5;

    /*
     * Max compressed size is equal to sum of:
     *   10 byte header
     *   8 byte foot
     *   max input expansion
     *   size of input
     */
    out_size = 10 + 8 + max_input_expansion + in_len;
    out_buf = flb_malloc(out_size);

    if (!out_buf) {
        flb_errno();
        flb_error("[gzip] could not allocate outgoing buffer");
        return -1;
    }

    /* Initialize streaming buffer context */
    memset(&strm, '\0', sizeof(strm));
    strm.zalloc    = Z_NULL;
    strm.zfree     = Z_NULL;
    strm.opaque    = Z_NULL;
    strm.next_in   = in_data;
    strm.avail_in  = in_len;
    strm.total_out = 0;

    /* Deflate mode */
    deflateInit2(&strm, Z_DEFAULT_COMPRESSION,
                 Z_DEFLATED, -Z_DEFAULT_WINDOW_BITS, 9, Z_DEFAULT_STRATEGY);

    /*
     * Miniz don't support GZip format directly, instead we will:
     *
     * - append manual GZip magic bytes
     * - deflate raw content
     * - append manual CRC32 data
     */
    gzip_header(out_buf);

    /* Header offset */
    pb = (uint8_t *) out_buf + FLB_GZIP_HEADER_OFFSET;

    flush = Z_NO_FLUSH;
    while (1) {
        strm.next_out  = pb + strm.total_out;
        strm.avail_out = out_size - (pb - (uint8_t *) out_buf);

        if (strm.avail_in == 0) {
            flush = Z_FINISH;
        }

        status = deflate(&strm, flush);
        if (status == Z_STREAM_END) {
            break;
        }
        else if (status != Z_OK) {
            deflateEnd(&strm);
            return -1;
        }
    }

    if (deflateEnd(&strm) != Z_OK) {
        flb_free(out_buf);
        return -1;
    }
    *out_len = strm.total_out;

    /* Construct the gzip checksum (CRC32 footer) */
    footer_start = FLB_GZIP_HEADER_OFFSET + *out_len;
    pb = (uint8_t *) out_buf + footer_start;

    crc = mz_crc32(MZ_CRC32_INIT, in_data, in_len);
    *pb++ = crc & 0xFF;
    *pb++ = (crc >> 8) & 0xFF;
    *pb++ = (crc >> 16) & 0xFF;
    *pb++ = (crc >> 24) & 0xFF;
    *pb++ = in_len & 0xFF;
    *pb++ = (in_len >> 8) & 0xFF;
    *pb++ = (in_len >> 16) & 0xFF;
    *pb++ = (in_len >> 24) & 0xFF;

    /* Set the real buffer size for the caller */
    *out_len += FLB_GZIP_HEADER_OFFSET + 8;
    *out_data = out_buf;

    return 0;
}


static unsigned int read_le16(const unsigned char *p)
{
    return ((unsigned int) p[0]) | ((unsigned int) p[1] << 8);
}


int flb_gzip_uncompress(void *in_data, size_t in_len,
                        void **out_data, size_t *out_len)
{
    int status;
    uint8_t *p;
    void *out_buf;
    size_t out_size = 0;
    void *zip_data;
    size_t zip_len;
    unsigned char flg;
    unsigned int xlen, hcrc;
    unsigned int dlen, crc;
    mz_ulong crc_out;
    mz_stream stream;
    const unsigned char *start;

    /* Minimal length: header + crc32 */
    if (in_len < 18) {
        flb_error("[gzip] unexpected content length");
        return -1;
    }

    /* Magic bytes */
    p = in_data;
    if (p[0] != 0x1F || p[1] != 0x8B) {
        flb_error("[gzip] invalid magic bytes");
        return -1;
    }

    if (p[2] != 8) {
        flb_error("[gzip] invalid method");
        return -1;
    }

    /* Flag byte */
    flg = p[3];

    /* Reserved bits */
    if (flg & 0xE0) {
        flb_error("[gzip] invalid flag");
        return -1;
    }

    /* Skip base header of 10 bytes */
    start = p + FLB_GZIP_HEADER_OFFSET;

    /* Skip extra data if present */
    if (flg & FEXTRA) {
        xlen = read_le16(start);
        if (xlen > in_len - 12) {
            flb_error("[gzip] invalid gzip data");
            return -1;
        }
        start += xlen + 2;
    }

    /* Skip file name if present */
    if (flg & FNAME) {
        do {
            if (start - p >= in_len) {
                flb_error("[gzip] invalid gzip data (FNAME)");
                return -1;
            }
        } while (*start++);
    }

    /* Skip file comment if present */
    if (flg & FCOMMENT) {
        do {
            if (start - p >= in_len) {
                flb_error("[gzip] invalid gzip data (FCOMMENT)");
                return -1;
            }
        } while (*start++);
    }

    /* Check header crc if present */
    if (flg & FHCRC) {
        if (start - p > in_len - 2) {
            flb_error("[gzip] invalid gzip data (FHRC)");
            return -1;
        }

        hcrc = read_le16(start);
        crc = mz_crc32(MZ_CRC32_INIT, p, start - p) & 0x0000FFFF;
        if (hcrc != crc) {
            flb_error("[gzip] invalid gzip header CRC");
            return -1;
        }
        start += 2;
    }

    /* Get decompressed length */
    dlen = read_le32(&p[in_len - 4]);

    /* Get CRC32 checksum of original data */
    crc = read_le32(&p[in_len - 8]);

    /* Decompress data */
    if ((p + in_len) - p < 8) {
        flb_error("[gzip] invalid gzip CRC32 checksum");
        return -1;
    }

    /* Allocate outgoing buffer */
    out_buf = flb_malloc(dlen);
    if (!out_buf) {
        flb_errno();
        return -1;
    }
    out_size = dlen;

    /* Map zip content */
    zip_data = (uint8_t *) start;
    zip_len = (p + in_len) - start - 8;

    memset(&stream, 0, sizeof(stream));
    stream.next_in = zip_data;
    stream.avail_in = zip_len;
    stream.next_out = out_buf;
    stream.avail_out = out_size;

    status = mz_inflateInit2(&stream, -Z_DEFAULT_WINDOW_BITS);
    if (status != MZ_OK) {
        flb_free(out_buf);
        return -1;
    }

    status = mz_inflate(&stream, MZ_FINISH);
    if (status != MZ_STREAM_END) {
        mz_inflateEnd(&stream);
        flb_free(out_buf);
        return -1;
    }

    if (stream.total_out != dlen) {
        mz_inflateEnd(&stream);
        flb_free(out_buf);
        flb_error("[gzip] invalid gzip data size");
        return -1;
    }

    /* terminate the stream, it's not longer required */
    mz_inflateEnd(&stream);

    /* Validate message CRC vs inflated data CRC */
    crc_out = mz_crc32(MZ_CRC32_INIT, out_buf, dlen);
    if (crc_out != crc) {
        flb_free(out_buf);
        flb_error("[gzip] invalid GZip checksum (CRC32)");
        return -1;
    }

    /* set the uncompressed data */
    *out_len = dlen;
    *out_data = out_buf;

    return 0;
}


processor_errfd(struct io *io, int evt, void *arg)
{
	const char	*name = arg;
	char		*line = NULL;
	ssize_t		 len;

	switch (evt) {
	case IO_DATAIN:
		while ((line = io_getline(io, &len)) != NULL)
			log_warnx("%s: %s", name, line);
	}
}

smtpd's filter state machine can prematurely release resources
leading to a crash.  From gilles@


filter_builtins_global(struct filter_session *fs, struct filter *filter, uint64_t reqid)
{
	return filter_check_fcrdns(filter, fs->fcrdns) ||
	    filter_check_rdns(filter, fs->rdns) ||
	    filter_check_rdns_table(filter, K_DOMAIN, fs->rdns) ||
	    filter_check_rdns_regex(filter, fs->rdns) ||
	    filter_check_src_table(filter, K_NETADDR, ss_to_text(&fs->ss_src)) ||
	    filter_check_src_regex(filter, ss_to_text(&fs->ss_src)) ||
	    filter_check_helo_table(filter, K_DOMAIN, fs->helo) ||
	    filter_check_helo_regex(filter, fs->helo) ||
	    filter_check_auth(filter, fs->username) ||
	    filter_check_auth_table(filter, K_STRING, fs->username) ||
	    filter_check_auth_table(filter, K_CREDENTIALS, fs->username) ||
	    filter_check_auth_regex(filter, fs->username) ||
	    filter_check_mail_from_table(filter, K_MAILADDR, fs->mail_from) ||
	    filter_check_mail_from_regex(filter, fs->mail_from);
}


filter_data_next(uint64_t token, uint64_t reqid, const char *line)
{
	struct filter_session  *fs;

	/* session can legitimately disappear on a resume */
	if ((fs = tree_get(&sessions, reqid)) == NULL)
		return;

	filter_data_internal(fs, token, reqid, line);
}


filter_protocol(uint64_t reqid, enum filter_phase phase, const char *param)
{
	struct filter_session  *fs;
	uint64_t		token = 0;
	char		       *nparam = NULL;

	fs = tree_xget(&sessions, reqid);

	switch (phase) {
	case FILTER_HELO:
	case FILTER_EHLO:
		free(fs->helo);
		fs->helo = xstrdup(param);
		break;
	case FILTER_MAIL_FROM:
		free(fs->mail_from);
		fs->mail_from = xstrdup(param + 1);
		*strchr(fs->mail_from, '>') = '\0';
		param = fs->mail_from;

		break;
	case FILTER_RCPT_TO:
		nparam = xstrdup(param + 1);
		*strchr(nparam, '>') = '\0';
		param = nparam;
		break;
	case FILTER_STARTTLS:
		/* TBD */
		break;
	default:
		break;
	}

	free(fs->lastparam);
	fs->lastparam = xstrdup(param);

	filter_protocol_internal(fs, &token, reqid, phase, param);
	if (nparam)
		free(nparam);
}


lka_proc_forked(const char *name, uint32_t subsystems, int fd)
{
	struct processor_instance	*processor;

	if (!processors_inited) {
		dict_init(&processors);
		processors_inited = 1;
	}

	processor = xcalloc(1, sizeof *processor);
	processor->name = xstrdup(name);
	processor->io = io_new();
	processor->subsystems = subsystems;

	io_set_nonblocking(fd);

	io_set_fd(processor->io, fd);
	io_set_callback(processor->io, processor_io, processor->name);
	dict_xset(&processors, name, processor);
}


processor_io(struct io *io, int evt, void *arg)
{
	struct processor_instance *processor;
	const char		*name = arg;
	char			*line = NULL;
	ssize_t			 len;

	switch (evt) {
	case IO_DATAIN:
		while ((line = io_getline(io, &len)) != NULL) {
			if (strncmp("register|", line, 9) == 0) {
				processor_register(name, line);
				continue;
			}

			processor = dict_xget(&processors, name);
			if (!processor->ready)
				fatalx("Non-register message before register|"
				    "ready: %s", line);
			else if (strncmp(line, "filter-result|", 14) == 0 ||
			    strncmp(line, "filter-dataline|", 16) == 0)
				lka_filter_process_response(name, line);
			else if (strncmp(line, "report|", 7) == 0)
				lka_report_proc(name, line);
			else
				fatalx("Invalid filter message type: %s", line);
		}
	}
}


filter_result_rewrite(uint64_t reqid, const char *param)
{
	m_create(p_pony, IMSG_FILTER_SMTP_PROTOCOL, 0, 0, -1);
	m_add_id(p_pony, reqid);
	m_add_int(p_pony, FILTER_REWRITE);
	m_add_string(p_pony, param);
	m_close(p_pony);
}


lka_report_smtp_link_identify(const char *direction, struct timeval *tv,
    uint64_t reqid, const char *method, const char *heloname)
{
	report_smtp_broadcast(reqid, direction, tv, "link-identify", "%s|%s\n",
	    method, heloname);
}


lka_filter_proc_in_session(uint64_t reqid, const char *proc)
{
	struct filter_session	*fs;
	struct filter		*filter;
	size_t			 i;

	if ((fs = tree_get(&sessions, reqid)) == NULL)
		return 0;

	filter = dict_get(&filters, fs->filter_name);
	if (filter == NULL || (filter->proc == NULL && filter->chain == NULL))
		return 0;

	if (filter->proc)
		return strcmp(filter->proc, proc) == 0 ? 1 : 0;

	for (i = 0; i < filter->chain_size; i++)
		if (filter->chain[i]->proc &&
		    strcmp(filter->chain[i]->proc, proc) == 0)
			return 1;

	return 0;
}


lka_report_smtp_link_auth(const char *direction, struct timeval *tv, uint64_t reqid,
    const char *username, const char *result)
{
	struct filter_session *fs;

	if (strcmp(result, "pass") == 0) {
		fs = tree_xget(&sessions, reqid);
		fs->username = xstrdup(username);
	}
	report_smtp_broadcast(reqid, direction, tv, "link-auth", "%s|%s\n",
	    username, result);
}


lka_filter_end(uint64_t reqid)
{
	struct filter_session	*fs;

	fs = tree_xpop(&sessions, reqid);
	free(fs->rdns);
	free(fs->helo);
	free(fs->mail_from);
	free(fs->username);
	free(fs->lastparam);
	free(fs->filter_name);
	free(fs);
	log_trace(TRACE_FILTERS, "%016"PRIx64" filters session-end", reqid);
}


filter_session_io(struct io *io, int evt, void *arg)
{
	struct filter_session *fs = arg;
	char *line = NULL;
	ssize_t len;

	log_trace(TRACE_IO, "filter session: %p: %s %s", fs, io_strevent(evt),
	    io_strio(io));

	switch (evt) {
	case IO_DATAIN:
	nextline:
		line = io_getline(fs->io, &len);
		/* No complete line received */
		if (line == NULL)
			return;

		filter_data(fs->id, line);

		goto nextline;
	}
}


filter_builtins_mail_from(struct filter_session *fs, struct filter *filter, uint64_t reqid, const char *param)
{
	return filter_builtins_global(fs, filter, reqid);
}


filter_check_auth_table(struct filter *filter, enum table_service kind, const char *key)
{
	int	ret = 0;

	if (filter->config->auth_table == NULL)
		return 0;

	if (key && table_match(filter->config->auth_table, kind, key) > 0)
		ret = 1;

	return filter->config->not_auth_table < 0 ? !ret : ret;
}


filter_check_src_table(struct filter *filter, enum table_service kind, const char *key)
{
	int	ret = 0;

	if (filter->config->src_table == NULL)
		return 0;

	if (table_match(filter->config->src_table, kind, key) > 0)
		ret = 1;
	return filter->config->not_src_table < 0 ? !ret : ret;
}


lka_report_smtp_tx_commit(const char *direction, struct timeval *tv, uint64_t reqid, uint32_t msgid, size_t msgsz)
{
	report_smtp_broadcast(reqid, direction, tv, "tx-commit", "%08x|%zd\n",
	    msgid, msgsz);
}


lka_filter_register_hook(const char *name, const char *hook)
{
	struct dict		*subsystem;
	struct filter		*filter;
	const char	*filter_name;
	void		*iter;
	size_t	i;

	if (strncasecmp(hook, "smtp-in|", 8) == 0) {
		subsystem = &filter_smtp_in;
		hook += 8;
	}
	else
		fatalx("Invalid message direction: %s", hook);

	for (i = 0; i < nitems(filter_execs); i++)
		if (strcmp(hook, filter_execs[i].phase_name) == 0)
			break;
	if (i == nitems(filter_execs))
		fatalx("Unrecognized report name: %s", hook);

	iter = NULL;
	while (dict_iter(&filters, &iter, &filter_name, (void **)&filter))
		if (filter->proc && strcmp(name, filter->proc) == 0)
			filter->phases |= (1<<filter_execs[i].phase);
}


filter_check_rdns(struct filter *filter, const char *hostname)
{
	int	ret = 0;
	struct netaddr	netaddr;

	if (!filter->config->rdns)
		return 0;

	/* this is a hack until smtp session properly deals with lack of rdns */
	ret = strcmp("<unknown>", hostname);
	if (ret == 0)
		return filter->config->not_rdns < 0 ? !ret : ret;

	/* if text_to_netaddress succeeds,
	 * we don't have an rDNS so the filter should match
	 */
	ret = !text_to_netaddr(&netaddr, hostname);
	return filter->config->not_rdns < 0 ? !ret : ret;
}


filter_check_rcpt_to_table(struct filter *filter, enum table_service kind, const char *key)
{
	int	ret = 0;

	if (filter->config->rcpt_to_table == NULL)
		return 0;

	if (table_match(filter->config->rcpt_to_table, kind, key) > 0)
		ret = 1;
	return filter->config->not_rcpt_to_table < 0 ? !ret : ret;
}


lka_report_smtp_timeout(const char *direction, struct timeval *tv, uint64_t reqid)
{
	report_smtp_broadcast(reqid, direction, tv, "timeout", "\n");
}


filter_data_query(struct filter *filter, uint64_t token, uint64_t reqid, const char *line)
{
	int	n;
	struct timeval	tv;

	gettimeofday(&tv, NULL);

	n = io_printf(lka_proc_get_io(filter->proc),
	    "filter|%s|%lld.%06ld|smtp-in|data-line|"
	    "%016"PRIx64"|%016"PRIx64"|%s\n",
	    PROTOCOL_VERSION,
	    tv.tv_sec, tv.tv_usec,
	    reqid, token, line);
	if (n == -1)
		fatalx("failed to write to processor");
}


filter_builtins_connect(struct filter_session *fs, struct filter *filter, uint64_t reqid, const char *param)
{
	return filter_builtins_global(fs, filter, reqid);
}


lka_proc_config(struct processor_instance *pi)
{
	io_printf(pi->io, "config|smtpd-version|%s\n", SMTPD_VERSION);
	io_printf(pi->io, "config|smtp-session-timeout|%d\n", SMTPD_SESSION_TIMEOUT);
	if (pi->subsystems & FILTER_SUBSYSTEM_SMTP_IN)
		io_printf(pi->io, "config|subsystem|smtp-in\n");
	if (pi->subsystems & FILTER_SUBSYSTEM_SMTP_OUT)
		io_printf(pi->io, "config|subsystem|smtp-out\n");
	io_printf(pi->io, "config|admd|%s\n",
	    env->sc_admd != NULL ? env->sc_admd : env->sc_hostname);
	io_printf(pi->io, "config|ready\n");
}


lka_report_smtp_link_connect(const char *direction, struct timeval *tv, uint64_t reqid, const char *rdns,
    int fcrdns,
    const struct sockaddr_storage *ss_src,
    const struct sockaddr_storage *ss_dest)
{
	struct filter_session *fs;
	char	src[NI_MAXHOST + 5];
	char	dest[NI_MAXHOST + 5];
	uint16_t	src_port = 0;
	uint16_t	dest_port = 0;
	const char     *fcrdns_str;

	if (ss_src->ss_family == AF_INET)
		src_port = ntohs(((const struct sockaddr_in *)ss_src)->sin_port);
	else if (ss_src->ss_family == AF_INET6)
		src_port = ntohs(((const struct sockaddr_in6 *)ss_src)->sin6_port);

	if (ss_dest->ss_family == AF_INET)
		dest_port = ntohs(((const struct sockaddr_in *)ss_dest)->sin_port);
	else if (ss_dest->ss_family == AF_INET6)
		dest_port = ntohs(((const struct sockaddr_in6 *)ss_dest)->sin6_port);

	if (strcmp(ss_to_text(ss_src), "local") == 0) {
		(void)snprintf(src, sizeof src, "unix:%s", SMTPD_SOCKET);
		(void)snprintf(dest, sizeof dest, "unix:%s", SMTPD_SOCKET);
	} else {
		(void)snprintf(src, sizeof src, "%s:%d", ss_to_text(ss_src), src_port);
		(void)snprintf(dest, sizeof dest, "%s:%d", ss_to_text(ss_dest), dest_port);
	}

	switch (fcrdns) {
	case 1:
		fcrdns_str = "pass";
		break;
	case 0:
		fcrdns_str = "fail";
		break;
	default:
		fcrdns_str = "error";
		break;
	}

	fs = tree_xget(&sessions, reqid);
	fs->rdns = xstrdup(rdns);
	fs->fcrdns = fcrdns;
	fs->ss_src = *ss_src;
	fs->ss_dest = *ss_dest;

	report_smtp_broadcast(reqid, direction, tv, "link-connect",
	    "%s|%s|%s|%s\n", rdns, fcrdns_str, src, dest);
}


lka_filter_ready(void)
{
	struct filter  	*filter;
	struct filter  	*subfilter;
	const char	*filter_name;
	struct filter_entry	*filter_entry;
	struct filter_chain	*filter_chain;
	void		*iter;
	size_t		i;
	size_t		j;

	/* all filters are ready, actually build the filter chains */
	iter = NULL;
	while (dict_iter(&filters, &iter, &filter_name, (void **)&filter)) {
		filter_chain = xcalloc(1, sizeof *filter_chain);
		for (i = 0; i < nitems(filter_execs); i++)
			TAILQ_INIT(&filter_chain->chain[i]);
		dict_set(&filter_chains, filter_name, filter_chain);

		if (filter->chain) {
			for (i = 0; i < filter->chain_size; i++) {
				subfilter = filter->chain[i];
				for (j = 0; j < nitems(filter_execs); ++j) {
					if (subfilter->phases & (1<<j)) {
						filter_entry = xcalloc(1, sizeof *filter_entry);
						filter_entry->id = generate_uid();
						filter_entry->name = subfilter->name;
						TAILQ_INSERT_TAIL(&filter_chain->chain[j],
						    filter_entry, entries);
					}
				}
			}
			continue;
		}

		for (i = 0; i < nitems(filter_execs); ++i) {
			if (filter->phases & (1<<i)) {
				filter_entry = xcalloc(1, sizeof *filter_entry);
				filter_entry->id = generate_uid();
				filter_entry->name = filter_name;
				TAILQ_INSERT_TAIL(&filter_chain->chain[i],
				    filter_entry, entries);
			}
		}
	}
}


filter_check_src_regex(struct filter *filter, const char *key)
{
	int	ret = 0;

	if (filter->config->src_regex == NULL)
		return 0;

	if (table_match(filter->config->src_regex, K_REGEX, key) > 0)
		ret = 1;
	return filter->config->not_src_regex < 0 ? !ret : ret;
}


filter_check_rdns_table(struct filter *filter, enum table_service kind, const char *key)
{
	int	ret = 0;

	if (filter->config->rdns_table == NULL)
		return 0;

	if (table_match(filter->config->rdns_table, kind, key) > 0)
		ret = 1;

	return filter->config->not_rdns_table < 0 ? !ret : ret;
}


filter_builtins_commit(struct filter_session *fs, struct filter *filter, uint64_t reqid, const char *param)
{
	return filter_builtins_global(fs, filter, reqid);
}


lka_report_init(void)
{
	struct reporters	*tailq;
	size_t			 i;

	dict_init(&report_smtp_in);
	dict_init(&report_smtp_out);

	for (i = 0; i < nitems(smtp_events); ++i) {
		tailq = xcalloc(1, sizeof (struct reporters));
		TAILQ_INIT(tailq);
		dict_xset(&report_smtp_in, smtp_events[i].event, tailq);

		tailq = xcalloc(1, sizeof (struct reporters));
		TAILQ_INIT(tailq);
		dict_xset(&report_smtp_out, smtp_events[i].event, tailq);
	}
}


filter_check_mail_from_regex(struct filter *filter, const char *key)
{
	int	ret = 0;

	if (filter->config->mail_from_regex == NULL)
		return 0;

	if (table_match(filter->config->mail_from_regex, K_REGEX, key) > 0)
		ret = 1;
	return filter->config->not_mail_from_regex < 0 ? !ret : ret;
}


filter_result_proceed(uint64_t reqid)
{
	m_create(p_pony, IMSG_FILTER_SMTP_PROTOCOL, 0, 0, -1);
	m_add_id(p_pony, reqid);
	m_add_int(p_pony, FILTER_PROCEED);
	m_close(p_pony);
}


lka_filter_data_end(uint64_t reqid)
{
	struct filter_session	*fs;

	fs = tree_xget(&sessions, reqid);
	if (fs->io) {
		io_free(fs->io);
		fs->io = NULL;
	}
	log_trace(TRACE_FILTERS, "%016"PRIx64" filters data-end", reqid);
}


filter_builtins_helo(struct filter_session *fs, struct filter *filter, uint64_t reqid, const char *param)
{
	return filter_builtins_global(fs, filter, reqid);
}


lka_report_smtp_tx_rcpt(const char *direction, struct timeval *tv, uint64_t reqid, uint32_t msgid, const char *address, int ok)
{
	const char *result;

	switch (ok) {
	case 1:
		result = "ok";
		break;
	case 0:
		result = "permfail";
		break;
	default:
		result = "tempfail";
		break;
	}
	report_smtp_broadcast(reqid, direction, tv, "tx-rcpt", "%08x|%s|%s\n",
	    msgid, result, address);
}


lka_filter_begin(uint64_t reqid, const char *filter_name)
{
	struct filter_session	*fs;

	if (!filters_inited) {
		tree_init(&sessions);
		filters_inited = 1;
	}

	fs = xcalloc(1, sizeof (struct filter_session));
	fs->id = reqid;
	fs->filter_name = xstrdup(filter_name);
	tree_xset(&sessions, fs->id, fs);

	log_trace(TRACE_FILTERS, "%016"PRIx64" filters session-begin", reqid);
}


filter_result_reject(uint64_t reqid, const char *message)
{
	m_create(p_pony, IMSG_FILTER_SMTP_PROTOCOL, 0, 0, -1);
	m_add_id(p_pony, reqid);
	m_add_int(p_pony, FILTER_REJECT);
	m_add_string(p_pony, message);
	m_close(p_pony);
}


lka_report_smtp_tx_mail(const char *direction, struct timeval *tv, uint64_t reqid, uint32_t msgid, const char *address, int ok)
{
	const char *result;

	switch (ok) {
	case 1:
		result = "ok";
		break;
	case 0:
		result = "permfail";
		break;
	default:
		result = "tempfail";
		break;
	}
	report_smtp_broadcast(reqid, direction, tv, "tx-mail", "%08x|%s|%s\n",
	    msgid, result, address);
}


lka_report_smtp_tx_begin(const char *direction, struct timeval *tv, uint64_t reqid, uint32_t msgid)
{
	report_smtp_broadcast(reqid, direction, tv, "tx-begin", "%08x\n",
	    msgid);
}


filter_protocol_next(uint64_t token, uint64_t reqid, enum filter_phase phase)
{
	struct filter_session  *fs;

	/* session can legitimately disappear on a resume */
	if ((fs = tree_get(&sessions, reqid)) == NULL)
		return;

	filter_protocol_internal(fs, &token, reqid, phase, fs->lastparam);
}


filter_check_helo_table(struct filter *filter, enum table_service kind, const char *key)
{
	int	ret = 0;

	if (filter->config->helo_table == NULL)
		return 0;

	if (table_match(filter->config->helo_table, kind, key) > 0)
		ret = 1;
	return filter->config->not_helo_table < 0 ? !ret : ret;
}


filter_builtins_rcpt_to(struct filter_session *fs, struct filter *filter, uint64_t reqid, const char *param)
{
	return filter_builtins_global(fs, filter, reqid) ||
	    filter_check_rcpt_to_table(filter, K_MAILADDR, param) ||
	    filter_check_rcpt_to_regex(filter, param);
}


filter_builtins_data(struct filter_session *fs, struct filter *filter, uint64_t reqid, const char *param)
{
	return filter_builtins_global(fs, filter, reqid);
}


lka_report_smtp_tx_reset(const char *direction, struct timeval *tv, uint64_t reqid, uint32_t msgid)
{
	report_smtp_broadcast(reqid, direction, tv, "tx-reset", "%08x\n",
	    msgid);
}


lka_filter_process_response(const char *name, const char *line)
{
	uint64_t reqid;
	uint64_t token;
	char buffer[LINE_MAX];
	char *ep = NULL;
	char *kind = NULL;
	char *qid = NULL;
	/*char *phase = NULL;*/
	char *response = NULL;
	char *parameter = NULL;
	struct filter_session *fs;

	(void)strlcpy(buffer, line, sizeof buffer);
	if ((ep = strchr(buffer, '|')) == NULL)
		fatalx("Missing token: %s", line);
	ep[0] = '\0';

	kind = buffer;

	qid = ep+1;
	if ((ep = strchr(qid, '|')) == NULL)
		fatalx("Missing reqid: %s", line);
	ep[0] = '\0';

	reqid = strtoull(qid, &ep, 16);
	if (qid[0] == '\0' || *ep != '\0')
		fatalx("Invalid reqid: %s", line);
	if (errno == ERANGE && reqid == ULLONG_MAX)
		fatal("Invalid reqid: %s", line);

	qid = ep+1;
	if ((ep = strchr(qid, '|')) == NULL)
		fatal("Missing directive: %s", line);
	ep[0] = '\0';

	token = strtoull(qid, &ep, 16);
	if (qid[0] == '\0' || *ep != '\0')
		fatalx("Invalid token: %s", line);
	if (errno == ERANGE && token == ULLONG_MAX)
		fatal("Invalid token: %s", line);

	response = ep+1;

	/* session can legitimately disappear on a resume */
	if ((fs = tree_get(&sessions, reqid)) == NULL)
		return;

	if (strcmp(kind, "filter-dataline") == 0) {
		if (fs->phase != FILTER_DATA_LINE)
			fatalx("filter-dataline out of dataline phase");
		filter_data_next(token, reqid, response);
		return;
	}
	if (fs->phase == FILTER_DATA_LINE)
		fatalx("filter-result in dataline phase");

	if ((ep = strchr(response, '|'))) {
		parameter = ep + 1;
		ep[0] = '\0';
	}

	if (strcmp(response, "proceed") == 0) {
		if (parameter != NULL)
			fatalx("Unexpected parameter after proceed: %s", line);
		filter_protocol_next(token, reqid, 0);
		return;
	} else if (strcmp(response, "junk") == 0) {
		if (parameter != NULL)
			fatalx("Unexpected parameter after junk: %s", line);
		if (fs->phase == FILTER_COMMIT)
			fatalx("filter-reponse junk after DATA");
		filter_result_junk(reqid);
		return;
	} else {
		if (parameter == NULL)
			fatalx("Missing parameter: %s", line);

		if (strcmp(response, "rewrite") == 0)
			filter_result_rewrite(reqid, parameter);
		else if (strcmp(response, "reject") == 0)
			filter_result_reject(reqid, parameter);
		else if (strcmp(response, "disconnect") == 0)
			filter_result_disconnect(reqid, parameter);
		else
			fatalx("Invalid directive: %s", line);
	}
}


filter_result_junk(uint64_t reqid)
{
	m_create(p_pony, IMSG_FILTER_SMTP_PROTOCOL, 0, 0, -1);
	m_add_id(p_pony, reqid);
	m_add_int(p_pony, FILTER_JUNK);
	m_close(p_pony);
}


filter_check_auth_regex(struct filter *filter, const char *key)
{
	int	ret = 0;

	if (filter->config->auth_regex == NULL)
		return 0;

	if (key && table_match(filter->config->auth_regex, K_REGEX, key) > 0)
		ret = 1;
	return filter->config->not_auth_regex < 0 ? !ret : ret;
}


filter_data_internal(struct filter_session *fs, uint64_t token, uint64_t reqid, const char *line)
{
	struct filter_chain	*filter_chain;
	struct filter_entry	*filter_entry;
	struct filter		*filter;

	if (!token)
		fs->phase = FILTER_DATA_LINE;
	if (fs->phase != FILTER_DATA_LINE)
		fatalx("misbehaving filter");

	/* based on token, identify the filter_entry we should apply  */
	filter_chain = dict_get(&filter_chains, fs->filter_name);
	filter_entry = TAILQ_FIRST(&filter_chain->chain[fs->phase]);
	if (token) {
		TAILQ_FOREACH(filter_entry, &filter_chain->chain[fs->phase], entries)
		    if (filter_entry->id == token)
			    break;
		if (filter_entry == NULL)
			fatalx("misbehaving filter");
		filter_entry = TAILQ_NEXT(filter_entry, entries);
	}

	/* no filter_entry, we either had none or reached end of chain */
	if (filter_entry == NULL) {
		io_printf(fs->io, "%s\n", line);
		return;
	}

	/* pass data to the filter */
	filter = dict_get(&filters, filter_entry->name);
	filter_data_query(filter, filter_entry->id, reqid, line);
}


lka_report_smtp_link_greeting(const char *direction, uint64_t reqid,
    struct timeval *tv, const char *domain)
{
	report_smtp_broadcast(reqid, direction, tv, "link-greeting", "%s\n",
	    domain);
}


lka_filter_data_begin(uint64_t reqid)
{
	struct filter_session  *fs;
	int	sp[2];
	int	fd = -1;

	fs = tree_xget(&sessions, reqid);

	if (socketpair(AF_UNIX, SOCK_STREAM, PF_UNSPEC, sp) == -1)
		goto end;
	io_set_nonblocking(sp[0]);
	io_set_nonblocking(sp[1]);
	fd = sp[0];
	fs->io = io_new();
	io_set_fd(fs->io, sp[1]);
	io_set_callback(fs->io, filter_session_io, fs);

end:
	m_create(p_pony, IMSG_FILTER_SMTP_DATA_BEGIN, 0, 0, fd);
	m_add_id(p_pony, reqid);
	m_add_int(p_pony, fd != -1 ? 1 : 0);
	m_close(p_pony);
	log_trace(TRACE_FILTERS, "%016"PRIx64" filters data-begin fd=%d", reqid, fd);
}


lka_report_proc(const char *name, const char *line)
{
	char buffer[LINE_MAX];
	struct timeval tv;
	char *ep, *sp, *direction;
	uint64_t reqid;

	if (strlcpy(buffer, line + 7, sizeof(buffer)) >= sizeof(buffer))
		fatalx("Invalid report: line too long: %s", line);

	errno = 0;
	tv.tv_sec = strtoll(buffer, &ep, 10);
	if (ep[0] != '.' || errno != 0)
		fatalx("Invalid report: invalid time: %s", line);
	sp = ep + 1;
	tv.tv_usec = strtol(sp, &ep, 10);
	if (ep[0] != '|' || errno != 0)
		fatalx("Invalid report: invalid time: %s", line);
	if (ep - sp != 6)
		fatalx("Invalid report: invalid time: %s", line);

	direction = ep + 1;
	if (strncmp(direction, "smtp-in|", 8) == 0) {
		direction[7] = '\0';
		direction += 7;
#if 0
	} else if (strncmp(direction, "smtp-out|", 9) == 0) {
		direction[8] = '\0';
		direction += 8;
#endif
	} else
		fatalx("Invalid report: invalid direction: %s", line);

	reqid = strtoull(sp, &ep, 16);
	if (ep[0] != '|' || errno != 0)
		fatalx("Invalid report: invalid reqid: %s", line);
	sp = ep + 1;

	lka_report_filter_report(reqid, name, 0, direction, &tv, sp);
}


lka_filter_protocol(uint64_t reqid, enum filter_phase phase, const char *param)
{
	filter_protocol(reqid, phase, param);
}


filter_check_rcpt_to_regex(struct filter *filter, const char *key)
{
	int	ret = 0;

	if (filter->config->rcpt_to_regex == NULL)
		return 0;

	if (table_match(filter->config->rcpt_to_regex, K_REGEX, key) > 0)
		ret = 1;
	return filter->config->not_rcpt_to_regex < 0 ? !ret : ret;
}


processor_register(const char *name, const char *line)
{
	struct processor_instance *processor;

	processor = dict_xget(&processors, name);

	if (strcmp(line, "register|ready") == 0) {
		processor->ready = 1;
		return;
	}

	if (strncmp(line, "register|report|", 16) == 0) {
		lka_report_register_hook(name, line+16);
		return;
	}

	if (strncmp(line, "register|filter|", 16) == 0) {
		lka_filter_register_hook(name, line+16);
		return;
	}

	fatalx("Invalid register line received: %s", line);
}


report_smtp_broadcast(uint64_t reqid, const char *direction, struct timeval *tv, const char *event,
    const char *format, ...)
{
	va_list		ap;
	struct dict	*d;
	struct reporters	*tailq;
	struct reporter_proc	*rp;

	if (strcmp("smtp-in", direction) == 0)
		d = &report_smtp_in;

	else if (strcmp("smtp-out", direction) == 0)
		d = &report_smtp_out;

	else
		fatalx("unexpected direction: %s", direction);

	tailq = dict_xget(d, event);
	TAILQ_FOREACH(rp, tailq, entries) {
		if (!lka_filter_proc_in_session(reqid, rp->name))
			continue;

		va_start(ap, format);
		if (io_printf(lka_proc_get_io(rp->name),
		    "report|%s|%lld.%06ld|%s|%s|%016"PRIx64"%s",
		    PROTOCOL_VERSION, tv->tv_sec, tv->tv_usec, direction,
		    event, reqid, format[0] != '\n' ? "|" : "") == -1 ||
		    io_vprintf(lka_proc_get_io(rp->name), format, ap) == -1)
			fatalx("failed to write to processor");
		va_end(ap);
	}
}


lka_proc_ready(void)
{
	void	*iter;
	struct processor_instance	*pi;

	iter = NULL;
	while (dict_iter(&processors, &iter, NULL, (void **)&pi))
		if (!pi->ready)
			return 0;
	return 1;
}


filter_builtins_notimpl(struct filter_session *fs, struct filter *filter, uint64_t reqid, const char *param)
{
	return 0;
}


filter_check_fcrdns(struct filter *filter, int fcrdns)
{
	int	ret = 0;

	if (!filter->config->fcrdns)
		return 0;

	ret = fcrdns == 1;
	return filter->config->not_fcrdns < 0 ? !ret : ret;
}


filter_check_mail_from_table(struct filter *filter, enum table_service kind, const char *key)
{
	int	ret = 0;

	if (filter->config->mail_from_table == NULL)
		return 0;

	if (table_match(filter->config->mail_from_table, kind, key) > 0)
		ret = 1;
	return filter->config->not_mail_from_table < 0 ? !ret : ret;
}


filter_protocol_query(struct filter *filter, uint64_t token, uint64_t reqid, const char *phase, const char *param)
{
	int	n;
	struct filter_session	*fs;
	struct timeval	tv;

	gettimeofday(&tv, NULL);

	fs = tree_xget(&sessions, reqid);
	if (strcmp(phase, "connect") == 0)
		n = io_printf(lka_proc_get_io(filter->proc),
		    "filter|%s|%lld.%06ld|smtp-in|%s|%016"PRIx64"|%016"PRIx64"|%s|%s\n",
		    PROTOCOL_VERSION,
		    tv.tv_sec, tv.tv_usec,
		    phase, reqid, token, fs->rdns, param);
	else
		n = io_printf(lka_proc_get_io(filter->proc),
		    "filter|%s|%lld.%06ld|smtp-in|%s|%016"PRIx64"|%016"PRIx64"|%s\n",
		    PROTOCOL_VERSION,
		    tv.tv_sec, tv.tv_usec,
		    phase, reqid, token, param);
	if (n == -1)
		fatalx("failed to write to processor");
}


lka_report_smtp_link_disconnect(const char *direction, struct timeval *tv, uint64_t reqid)
{
	report_smtp_broadcast(reqid, direction, tv, "link-disconnect", "\n");
}


lka_report_smtp_tx_data(const char *direction, struct timeval *tv, uint64_t reqid, uint32_t msgid, int ok)
{
	const char *result;

	switch (ok) {
	case 1:
		result = "ok";
		break;
	case 0:
		result = "permfail";
		break;
	default:
		result = "tempfail";
		break;
	}
	report_smtp_broadcast(reqid, direction, tv, "tx-data", "%08x|%s\n",
	    msgid, result);
}


filter_result_disconnect(uint64_t reqid, const char *message)
{
	m_create(p_pony, IMSG_FILTER_SMTP_PROTOCOL, 0, 0, -1);
	m_add_id(p_pony, reqid);
	m_add_int(p_pony, FILTER_DISCONNECT);
	m_add_string(p_pony, message);
	m_close(p_pony);
}


filter_check_helo_regex(struct filter *filter, const char *key)
{
	int	ret = 0;

	if (filter->config->helo_regex == NULL)
		return 0;

	if (table_match(filter->config->helo_regex, K_REGEX, key) > 0)
		ret = 1;
	return filter->config->not_helo_regex < 0 ? !ret : ret;
}


lka_report_register_hook(const char *name, const char *hook)
{
	struct dict	*subsystem;
	struct reporter_proc	*rp;
	struct reporters	*tailq;
	void *iter;
	size_t	i;

	if (strncmp(hook, "smtp-in|", 8) == 0) {
		subsystem = &report_smtp_in;
		hook += 8;
	}
	else if (strncmp(hook, "smtp-out|", 9) == 0) {
		subsystem = &report_smtp_out;
		hook += 9;
	}
	else
		fatalx("Invalid message direction: %s", hook);

	if (strcmp(hook, "*") == 0) {
		iter = NULL;
		while (dict_iter(subsystem, &iter, NULL, (void **)&tailq)) {
			rp = xcalloc(1, sizeof *rp);
			rp->name = xstrdup(name);
			TAILQ_INSERT_TAIL(tailq, rp, entries);
		}
		return;
	}

	for (i = 0; i < nitems(smtp_events); i++)
		if (strcmp(hook, smtp_events[i].event) == 0)
			break;
	if (i == nitems(smtp_events))
		fatalx("Unrecognized report name: %s", hook);

	tailq = dict_get(subsystem, hook);
	rp = xcalloc(1, sizeof *rp);
	rp->name = xstrdup(name);
	TAILQ_INSERT_TAIL(tailq, rp, entries);
}


filter_protocol_internal(struct filter_session *fs, uint64_t *token, uint64_t reqid, enum filter_phase phase, const char *param)
{
	struct filter_chain	*filter_chain;
	struct filter_entry	*filter_entry;
	struct filter		*filter;
	struct timeval		 tv;
	const char		*phase_name = filter_execs[phase].phase_name;
	int			 resume = 1;

	if (!*token) {
		fs->phase = phase;
		resume = 0;
	}

	/* XXX - this sanity check requires a protocol change, stub for now */
	phase = fs->phase;
	if (fs->phase != phase)
		fatalx("misbehaving filter");

	/* based on token, identify the filter_entry we should apply  */
	filter_chain = dict_get(&filter_chains, fs->filter_name);
	filter_entry = TAILQ_FIRST(&filter_chain->chain[fs->phase]);
	if (*token) {
		TAILQ_FOREACH(filter_entry, &filter_chain->chain[fs->phase], entries)
		    if (filter_entry->id == *token)
			    break;
		if (filter_entry == NULL)
			fatalx("misbehaving filter");
		filter_entry = TAILQ_NEXT(filter_entry, entries);
	}

	/* no filter_entry, we either had none or reached end of chain */
	if (filter_entry == NULL) {
		log_trace(TRACE_FILTERS, "%016"PRIx64" filters protocol phase=%s, resume=%s, "
		    "action=proceed",
		    fs->id, phase_name, resume ? "y" : "n");
		filter_result_proceed(reqid);
		return;
	}

	/* process param with current filter_entry */
	*token = filter_entry->id;
	filter = dict_get(&filters, filter_entry->name);
	if (filter->proc) {
		log_trace(TRACE_FILTERS, "%016"PRIx64" filters protocol phase=%s, "
		    "resume=%s, action=deferred, filter=%s",
		    fs->id, phase_name, resume ? "y" : "n",
		    filter->name);
		filter_protocol_query(filter, filter_entry->id, reqid,
		    filter_execs[fs->phase].phase_name, param);
		return;	/* deferred response */
	}

	if (filter_execs[fs->phase].func(fs, filter, reqid, param)) {
		if (filter->config->rewrite) {
			log_trace(TRACE_FILTERS, "%016"PRIx64" filters protocol phase=%s, "
			    "resume=%s, action=rewrite, filter=%s, query=%s, response=%s",
			    fs->id, phase_name, resume ? "y" : "n",
			    filter->name,
			    param,
			    filter->config->rewrite);
			    filter_result_rewrite(reqid, filter->config->rewrite);
			return;
		}
		else if (filter->config->disconnect) {
			log_trace(TRACE_FILTERS, "%016"PRIx64" filters protocol phase=%s, "
			    "resume=%s, action=disconnect, filter=%s, query=%s, response=%s",
			    fs->id, phase_name, resume ? "y" : "n",
			    filter->name,
			    param,
			    filter->config->disconnect);
			filter_result_disconnect(reqid, filter->config->disconnect);
			return;
		}
		else if (filter->config->junk) {
			log_trace(TRACE_FILTERS, "%016"PRIx64" filters protocol phase=%s, "
			    "resume=%s, action=junk, filter=%s, query=%s",
			    fs->id, phase_name, resume ? "y" : "n",
			    filter->name,
			    param);
			filter_result_junk(reqid);
			return;
		} else if (filter->config->report) {
			log_trace(TRACE_FILTERS, "%016"PRIx64" filters protocol phase=%s, "
			    "resume=%s, action=report, filter=%s, query=%s response=%s",
			    fs->id, phase_name, resume ? "y" : "n",
			    filter->name,
			    param, filter->config->report);

			gettimeofday(&tv, NULL);
			lka_report_filter_report(fs->id, filter->name, 1,
			    "smtp-in", &tv, filter->config->report);
		} else if (filter->config->bypass) {
			log_trace(TRACE_FILTERS, "%016"PRIx64" filters protocol phase=%s, "
			    "resume=%s, action=bypass, filter=%s, query=%s",
			    fs->id, phase_name, resume ? "y" : "n",
			    filter->name,
			    param);
			filter_result_proceed(reqid);
			return;
		} else {
			log_trace(TRACE_FILTERS, "%016"PRIx64" filters protocol phase=%s, "
			    "resume=%s, action=reject, filter=%s, query=%s, response=%s",
			    fs->id, phase_name, resume ? "y" : "n",
			    filter->name,
			    param,
			    filter->config->reject);
			filter_result_reject(reqid, filter->config->reject);
			return;
		}
	}

	log_trace(TRACE_FILTERS, "%016"PRIx64" filters protocol phase=%s, "
	    "resume=%s, action=proceed, filter=%s, query=%s",
	    fs->id, phase_name, resume ? "y" : "n",
	    filter->name,
	    param);

	/* filter_entry resulted in proceed, try next filter */
	filter_protocol_internal(fs, token, reqid, phase, param);
	return;
}


filter_check_auth(struct filter *filter, const char *username)
{
	int ret = 0;

	if (!filter->config->auth)
		return 0;

	ret = username ? 1 : 0;

	return filter->config->not_auth < 0 ? !ret : ret;
}


filter_check_rdns_regex(struct filter *filter, const char *key)
{
	int	ret = 0;

	if (filter->config->rdns_regex == NULL)
		return 0;

	if (table_match(filter->config->rdns_regex, K_REGEX, key) > 0)
		ret = 1;
	return filter->config->not_rdns_regex < 0 ? !ret : ret;
}


lka_report_smtp_tx_rollback(const char *direction, struct timeval *tv, uint64_t reqid, uint32_t msgid)
{
	report_smtp_broadcast(reqid, direction, tv, "tx-rollback", "%08x\n",
	    msgid);
}


lka_proc_get_io(const char *name)
{
	struct processor_instance *processor;

	processor = dict_xget(&processors, name);

	return processor->io;
}


lka_report_filter_report(uint64_t reqid, const char *name, int builtin,
    const char *direction, struct timeval *tv, const char *message)
{
	report_smtp_broadcast(reqid, direction, tv, "filter-report",
	    "%s|%s|%s\n", builtin ? "builtin" : "proc",
	    name, message);
}


lka_filter_init(void)
{
	void		*iter;
	const char	*name;
	struct filter  	*filter;
	struct filter_config	*filter_config;
	size_t		i;
	char		 buffer[LINE_MAX];	/* for traces */

	dict_init(&filters);
	dict_init(&filter_chains);

	/* first pass, allocate and init individual filters */
	iter = NULL;
	while (dict_iter(env->sc_filters_dict, &iter, &name, (void **)&filter_config)) {
		switch (filter_config->filter_type) {
		case FILTER_TYPE_BUILTIN:
			filter = xcalloc(1, sizeof(*filter));
			filter->name = name;
			filter->phases |= (1<<filter_config->phase);
			filter->config = filter_config;
			dict_set(&filters, name, filter);
			log_trace(TRACE_FILTERS, "filters init type=builtin, name=%s, hooks=%08x",
			    name, filter->phases);
			break;

		case FILTER_TYPE_PROC:
			filter = xcalloc(1, sizeof(*filter));
			filter->name = name;
			filter->proc = filter_config->proc;
			filter->config = filter_config;
			dict_set(&filters, name, filter);
			log_trace(TRACE_FILTERS, "filters init type=proc, name=%s, proc=%s",
			    name, filter_config->proc);
			break;

		case FILTER_TYPE_CHAIN:
			break;
		}
	}

	/* second pass, allocate and init filter chains but don't build yet */
	iter = NULL;
	while (dict_iter(env->sc_filters_dict, &iter, &name, (void **)&filter_config)) {
		switch (filter_config->filter_type) {
		case FILTER_TYPE_CHAIN:
			filter = xcalloc(1, sizeof(*filter));
			filter->name = name;
			filter->chain = xcalloc(filter_config->chain_size, sizeof(void **));
			filter->chain_size = filter_config->chain_size;
			filter->config = filter_config;

			buffer[0] = '\0';
			for (i = 0; i < filter->chain_size; ++i) {
				filter->chain[i] = dict_xget(&filters, filter_config->chain[i]);
				if (i)
					(void)strlcat(buffer, ", ", sizeof buffer);
				(void)strlcat(buffer, filter->chain[i]->name, sizeof buffer);
			}
			log_trace(TRACE_FILTERS, "filters init type=chain, name=%s { %s }", name, buffer);

			dict_set(&filters, name, filter);
			break;

		case FILTER_TYPE_BUILTIN:
		case FILTER_TYPE_PROC:
			break;
		}
	}
}


lka_report_smtp_tx_envelope(const char *direction, struct timeval *tv, uint64_t reqid, uint32_t msgid, uint64_t evpid)
{
	report_smtp_broadcast(reqid, direction, tv, "tx-envelope",
	    "%08x|%016"PRIx64"\n", msgid, evpid);
}


filter_data(uint64_t reqid, const char *line)
{
	struct filter_session  *fs;

	fs = tree_xget(&sessions, reqid);

	filter_data_internal(fs, 0, reqid, line);
}


lka_report_smtp_protocol_server(const char *direction, struct timeval *tv, uint64_t reqid, const char *response)
{
	report_smtp_broadcast(reqid, direction, tv, "protocol-server", "%s\n",
	    response);
}


lka_proc_errfd(const char *name, int fd)
{
	struct processor_instance	*processor;

	processor = dict_xget(&processors, name);

	io_set_nonblocking(fd);

	processor->errfd = io_new();
	io_set_fd(processor->errfd, fd);
	io_set_callback(processor->errfd, processor_errfd, processor->name);

	lka_proc_config(processor);
}


lka_report_smtp_filter_response(const char *direction, struct timeval *tv, uint64_t reqid,
    int phase, int response, const char *param)
{
	const char *phase_name;
	const char *response_name;

	switch (phase) {
	case FILTER_CONNECT:
		phase_name = "connected";
		break;
	case FILTER_HELO:
		phase_name = "helo";
		break;
	case FILTER_EHLO:
		phase_name = "ehlo";
		break;
	case FILTER_STARTTLS:
		phase_name = "tls";
		break;
	case FILTER_AUTH:
		phase_name = "auth";
		break;
	case FILTER_MAIL_FROM:
		phase_name = "mail-from";
		break;
	case FILTER_RCPT_TO:
		phase_name = "rcpt-to";
		break;
	case FILTER_DATA:
		phase_name = "data";
		break;
	case FILTER_DATA_LINE:
		phase_name = "data-line";
		break;
	case FILTER_RSET:
		phase_name = "rset";
		break;
	case FILTER_QUIT:
		phase_name = "quit";
		break;
	case FILTER_NOOP:
		phase_name = "noop";
		break;
	case FILTER_HELP:
		phase_name = "help";
		break;
	case FILTER_WIZ:
		phase_name = "wiz";
		break;
	case FILTER_COMMIT:
		phase_name = "commit";
		break;
	default:
		phase_name = "";
	}

	switch (response) {
	case FILTER_PROCEED:
		response_name = "proceed";
		break;
	case FILTER_JUNK:
		response_name = "junk";
		break;
	case FILTER_REWRITE:
		response_name = "rewrite";
		break;
	case FILTER_REJECT:
		response_name = "reject";
		break;
	case FILTER_DISCONNECT:
		response_name = "disconnect";
		break;
	default:
		response_name = "";
	}

	report_smtp_broadcast(reqid, direction, tv, "filter-response",
	    "%s|%s%s%s\n", phase_name, response_name, param ? "|" : "",
	    param ? param : "");
}


lka_report_smtp_protocol_client(const char *direction, struct timeval *tv, uint64_t reqid, const char *command)
{
	report_smtp_broadcast(reqid, direction, tv, "protocol-client", "%s\n",
	    command);
}


lka_report_smtp_link_tls(const char *direction, struct timeval *tv, uint64_t reqid, const char *ciphers)
{
	report_smtp_broadcast(reqid, direction, tv, "link-tls", "%s\n",
	    ciphers);
}