#include <../source/TAL/objbase.h>
#include <../source/PAL/dataman.h>

/*
 * TODO:
 * add support for binary hive format (M$ format ?)
 * hash tables / sorting for every section ?
 * reg_type_float ?
 * reg_type_callback / attached callbacks ?
 * consider using hash table for entire registry (see devel/vmap)
 * reduce code complexity !
 * consider using vfs tree for config data ('//./etc' ?); resolve
 *  locking issues (really need read-write lock !).
 */

/*
 *	Registry types
 */
typedef enum
{
	reg_type_dword,
	reg_type_string,
	reg_type_binary
}	reg_type_t;

/*
 *	Registry value
 */
typedef struct	reg_value_s
{
	reg_type_t	type;
	u32_t		data_size; /* for binaries */
	char		*name;
	void		*data;
}	reg_value_t, *reg_value_p;

/*
 *	Registry section to store in <ntree_t*> -> data.
 */
typedef struct reg_section_s
{
	char		*name;			/* Name of THIS node */
	vp_stack_t	values;
}	reg_section_t, *reg_section_p;

/*	======================================================================
 *	Helper functions.
 */

/*
 *	Allocates memory for value and data, stores type,
 *	copies data_size bytes into result->data.
 */
V_LOCAL reg_value_p
regAllocValue(reg_type_t type,char *name,void *data,u32_t data_size)
{
	reg_value_p v;
	u32_t n_len,na_len,da_len;
	n_len=strlen(name)+1;
	/* Align lengths on dword boundary, alocate storage */
	na_len=INT_ALIGN(n_len);
	da_len=INT_ALIGN(data_size);
	v=(reg_value_p)memAlloc(sizeof(reg_value_t)+na_len+da_len);
	v->type=type;
	v->data_size=data_size;
	v->name=(char*)(v+1);
	v->data=(void*)(v->name+na_len);
	memCopy(v->name,name,n_len);
	memCopy(v->data,data,v->data_size);
	return v;
}

/*
 *	Allocates new registry section with given name.
 */
V_LOCAL	reg_section_p
regAllocSection(char *name)
{
	u32_t len;
	reg_section_t *rs;
	len=strlen(name)+1;
	rs=(reg_section_t*)memAlloc(sizeof(reg_section_t)+len);
	rs->name=(char*)(rs+1);
	memCopy(rs->name,name,len);
	vpsInit(&rs->values);
	return rs;
}

/*
 *	Removes all values and memFree()'s section.
 *	This is the default destructor for registry tree.
 */
void	V_LOCAL	V_CALL
regDeleteSection(/*reg_section_p*/void *rvs)
{
	u32_t i,vc;
	reg_value_t **rv;
	reg_section_t *rs=(reg_section_t*)rvs;
	vc=vpsCount(&rs->values);
	rv=(reg_value_t**)vpsGetStorage(&rs->values);
	for(i=0;i<vc;i++)
		if(rv[i])
			memFree(rv[i]);
	vpsClear(&rs->values);
	memFree(rs);
}

/*
 *	Searches in section for value with given key and type.
 */
V_LOCAL	reg_value_p
regFindValueInSiblings(reg_section_t *section,char *key,reg_type_t type)
{
	u32_t i,nvals;
	reg_value_t **vals;
	vals=(reg_value_t**)vpsGetStorage(&section->values);
	nvals=vpsCount(&section->values);
	for(i=0;i<nvals;i++)
		if(!strcmp(key,vals[i]->name) && type==vals[i]->type)
			break;
	return i==nvals ? NULL : vals[i];
}

/*
 *	Deletes given value
 */
V_LOCAL	vboolean
regDeleteValueInSection(reg_section_t *section,char *key)
{
	u32_t i,nvals;
	reg_value_t **vals;
	vals=(reg_value_t**)vpsGetStorage(&section->values);
	nvals=vpsCount(&section->values);
	for(i=0;i<nvals;i++)
		if(!strcmp(key,vals[i]->name))
			break;
	if(i<nvals)
	{
		void *dummy;
		/* delete entry */
		memFree(vals[i]);
		/* swap values to eliminate hole */
		vals[i]=vals[nvals-1];
		/* reduce stack counter */
		vpsPop(&section->values,&dummy);
		return true;
	}
	return false;
}

/*
 * Stores path from root to given registry node in given stack.
 * FIXME: higher-then-root check !
 */
void	V_LOCAL
regAbsolutePath(u8_stack_t *dest,ntree_t *node)
{
	vp_stack_t names;
	reg_section_t *rs;
	u8sClear(dest);
	vpsInit(&names);
	/* store node list: from current up to root's child */
	while(node->parent)
	{
		vpsPush(&names,node->data);
		node=node->parent;
	}
	/* get all names from node list */
	u8sPush(dest,'\\');	/* name of root node */
	while(vpsPop(&names,(void_p*)&rs))
	{
		u8sPushBuffer(dest,(u8_t*)rs->name,(u32_t)strlen(rs->name));
		u8sPush(dest,'\\');
	}
	/* strip last slash if strlen > 1 */
	if(u8sCount(dest)>2)
	{
		u8_t dummy;
		u8sPop(dest,&dummy);
	}
	u8sPush(dest,0);
	vpsClear(&names);
	return;
}

V_LOCAL	reg_section_p
#ifdef	CHECKED_BUILD
regGetSection(char *caller)
#else	/* !CHECKED_BUILD */
regGetSectionQuiet(void)
#define	regGetSection(caller)	regGetSectionQuiet()
#endif	/* !CHECKED_BUILD */
{
	global_data_t *gd;
	gd=libvLockGlobal();
	if(!gd->current_key)
	{
		libvUnlockGlobal();
#ifdef	CHECKED_BUILD
		conlPrintf(LOG_ERROR,"%s(): registry not initialized.\n",
			caller);
#endif	/* CHECKED_BUILD */
		return NULL;
	}
	return (reg_section_t*)(gd->current_key->data);
	/* NOTE: global data locked ! */
}

#ifdef	CHECKED_BUILD
/*
 *	Stores absolute name of current registry key in 'dest', returns
 *	minimal storage length for name (can be more than 'dest_len').
 */
u32_t	V_LOCAL
regCurrentKey(char *dest,u32_t dest_len)
{
	ntree_t *regnode;
	u8_stack_t name;
	regnode=libvLockGlobal()->current_key;
	if(!regnode)
	{
		libvUnlockGlobal();
		conlPuts(LOG_ERROR,"regCurrentKey(): registry not initialized.\n");
		return 0;
	}
	u8sInit(&name);
	regAbsolutePath(&name,regnode);
	strCopy(dest,(char*)u8sGetStorage(&name),dest_len);
	libvUnlockGlobal();
	dest_len=u8sCount(&name);
	u8sClear(&name);
	return dest_len;
}
#endif	/* CHECKED_BUILD */

V_LOCAL	reg_value_p
#ifdef	CHECKED_BUILD
regGetExistingValue(char *caller,char *value_key,reg_type_t type)
#else	/* !CHECKED_BUILD */
regGetExistingValueQuiet(char *value_key,reg_type_t type)
#define	regGetExistingValue(caller,value_key,type) \
		regGetExistingValueQuiet(value_key,type)
#endif	/* !CHECKED_BUILD */
{
	reg_value_t *rv;
	reg_section_t *cs;
	cs=regGetSection(caller);
	if(!cs)
		return NULL;
	rv=regFindValueInSiblings(cs,value_key,type);
	if(!rv)
	{
#ifdef	CHECKED_BUILD
		char curpath[1024];
		char *t_id;
#endif	/* CHECKED_BUILD */
		libvUnlockGlobal();
#ifdef	CHECKED_BUILD
		switch(type)
		{
			case reg_type_dword: t_id="DWORD"; break;
			case reg_type_string: t_id="STRING"; break;
			case reg_type_binary: t_id="BINARY"; break;
			default: t_id="[UNKNOWN TYPE]"; break;
		}
		regCurrentKey(curpath,1024);
		conlPrintf(LOG_ERROR,"%s(): no %s value '%s' in key '%s'.\n",
			caller,t_id,value_key,curpath);
#endif	/* CHECKED_BUILD */
		return NULL;
	}
	return rv;
	/* NOTE: global data locked ! */
}

/*
 *	Stores new data, deteles old.
 */
vboolean	V_LOCAL
#ifdef	CHECKED_BUILD
regReplaceValue(char *caller,char *value_key,reg_type_t type,
	void *data,u32_t data_size)
#else	/* !CHECKED_BUILD */
regReplaceValueQuiet(char *value_key,reg_type_t type,
	void *data,u32_t data_size)
#define	regReplaceValue(caller,value_key,type,data,data_size) \
		regReplaceValueQuiet(value_key,type,data,data_size)
#endif	/* !CHECKED_BUILD */
{
	reg_section_t *cs;
	cs=regGetSection(caller);
	if(!cs)
		return false;
	regDeleteValueInSection(cs,value_key);
	switch(type)
	{
		case reg_type_dword:
			vpsPush(&cs->values,
				regAllocValue(reg_type_dword,value_key,data,data_size));
			break;
		case reg_type_string:
			vpsPush(&cs->values,
				regAllocValue(reg_type_string,value_key,data,data_size));
			break;
		case reg_type_binary:
			vpsPush(&cs->values,
				regAllocValue(reg_type_binary,value_key,data,data_size));
			break;
		default:
			libvUnlockGlobal();
			return false;
	}
	libvUnlockGlobal();
	return true;
}

/*
 *	Stores given node's section and all section of child nodes
 *	in file 'out'.
 */
void	V_LOCAL
regDumpSection_r(vhandle_t out,ntree_t *node)
{
	u32_t i,j,len;
	reg_value_t *rv;
	ntree_t **cnodes;
	reg_value_t **rvs;
	reg_section_t *rs;
	u8_stack_t node_name;
	rs=(reg_section_t*)node->data;
	u8sInit(&node_name);
	regAbsolutePath(&node_name,node);
	filePrintf(out,"[%s]\n",(char*)u8sGetStorage(&node_name)+1);
	u8sClear(&node_name);
	len=vpsCount(&rs->values);
	rvs=(reg_value_t**)vpsGetStorage(&rs->values);
	for(i=0;i<len;i++)
	{
		rv=rvs[i];
		filePrintf(out,"\"%s\"=",rv->name);
		switch(rv->type)
		{
			case reg_type_dword:
				filePrintf(out,"dword:%08x\n",*((unsigned*)rv->data));
				break;
			case reg_type_string:
				filePrintf(out,"\"%s\"\n",(char*)rv->data);
				break;
			case reg_type_binary:
				filePrintf(out,"hex:");
				for(j=0;j<rv->data_size-1;j++)
					filePrintf(out,"%02x,",((u8_t*)rv->data)[j]);
				filePrintf(out,"%02x\n",((u8_t*)rv->data)[j]);
				break;
			default:
				break;
		}
	}
	filePrintf(out,"\n");
	/* Dump all children */
	len=vpsCount(&node->children);
	cnodes=(ntree_t**)vpsGetStorage(&node->children);
	for(i=0;i<len;i++)
		regDumpSection_r(out,cnodes[i]);
	return;
}

/*
 * Registry is n-way tree with data types of reg_section_p. Root node
 * contains 2 children with empty entries named HKEY_CURRENT_USER and 
 * HKEY_LOCAL_MACHINE. (FIXME ?)
 */

/*
 *	Deletes registry database.
 */
void	V_GLOBAL
regShutdown(void)
{
	global_data_t *gd;
	gd=libvLockGlobal();
	if(gd->reg_root)
	{
		treeDeleteAllNodes(gd->reg_root,regDeleteSection);
		gd->reg_root=gd->current_key=NULL;
	}
	libvUnlockGlobal();
	return;
}

/*
 *	Creates emply registry
 */
void	V_GLOBAL
regCreate(void)
{
	global_data_t *gd=libvLockGlobal();
	if(gd->reg_root)
	{
#ifdef	CHECKED_BUILD
		conlPuts(LOG_WARNING,
			"regCreateRegistry(): erasing current database.\n");
#endif	/* CHECKED_BUILD */
		treeDeleteAllNodes(gd->reg_root,regDeleteSection);
	}
	gd->reg_root=treeAllocNode(regAllocSection("/"),NULL);
	gd->current_key=treeAllocNode(regAllocSection("HKEY_CURRENT_USER"),
		gd->reg_root);
	treeAllocNode(regAllocSection("HKEY_LOCAL_MACHINE"),
		gd->reg_root);
	libvUnlockGlobal();
	return;
}

/*
 *	Opens/Creates given key
 */
vboolean	V_GLOBAL
regSetKey(char *key_name)
{
	global_data_t *gd;
	vhandle_t pp;		/* path parser */
	ntree_t *node;
	ntree_t **cnodes;	/* child nodes of 'node' */
	u32_t i,nc;			/* number of child nodes */
	char *k;			/* next part of key name */
	if(!key_name)
	{
		/* wrong key name */
		return false;
	}
	gd=libvLockGlobal();
	if(!gd->reg_root)
		regCreate();
	if(key_name[0]=='\\' || key_name[0]=='/')	/* absolute path */
		node=gd->reg_root;
	else					/* relative path */
		node=gd->current_key;
	/*	Parse path */
	pp = lexParseString(key_name,"/\\");
	lexDisable(pp,LEX_DEFAULT_DELIMITERS);
	while(lexNext(pp))
	{
		k = lexLast(pp);
		if((k[0]=='/' || k[0]=='\\') && k[1]==0)
			continue;
		if(k[0]=='.' && k[1]==0)
			continue;
		if(k[0]=='.' && k[1]=='.' && k[2]==0)
		{
			/* move to root */
			if(!node->parent)
			{
				libvUnlockGlobal();
#ifdef			CHECKED_BUILD
				{
				char curpath[1024];
				regCurrentKey(curpath,1024);
				conlPrintf(LOG_ERROR,"regSetKey('%s'): malformed path for"
					" current key '%s'.\n",key_name,curpath);
				}
#endif			/* CHECKED_BUILD */
				lexDestroy(pp);
				return false;
			}
			node=node->parent;
			continue;
		}
		/* try to find section with given name */
		nc=vpsCount(&node->children);
		cnodes=(ntree_t**)vpsGetStorage(&node->children);
		for(i=0;i<nc;i++)
			if(!strcmp(((reg_section_t*)cnodes[i]->data)->name,k))
				break;
		if(i==nc)
		{
			/* No such key: create it ! */
			node=treeAllocNode(regAllocSection(k),node);
#ifdef		CHECKED_BUILD
			conlPrintf(LOG_TRACE,"regSetKey('%s'): created subkey '%s'.\n",
				key_name,k);
#endif		/* CHECKED_BUILD */
		}
		else
			node=cnodes[i];
	}
	lexDestroy(pp);
	gd->current_key=node;
	libvUnlockGlobal();
	return true;
}

vboolean	V_GLOBAL
regGetDword(char *value_key,u32_t *dest)
{
	reg_value_t *rv;
	if(!value_key || !dest)
	{
#ifdef	CHECKED_BUILD
		conlPrintf(LOG_ERROR,
			"regGetDword(%s,%X): incorrect parameter(s).\n",
			strSafe(value_key),dest);
#endif	/* CHECKED_BUILD */
		return false;
	}
	rv=regGetExistingValue("regGetDword",value_key,reg_type_dword);
	if(!rv)
		return false;
	*dest=*((u32_t*)rv->data);
	libvUnlockGlobal();
	return true;
}

vboolean	V_GLOBAL
regGetString(char *value_key,char *dest,u32_t dest_size)
{
	reg_value_t *rv;
	vboolean res;
	if(!value_key || !dest || !dest_size)
	{
#ifdef	CHECKED_BUILD
		conlPrintf(LOG_ERROR,
			"regGetString(%s,%X,%u): incorrect parameter(s).\n",
			strSafe(value_key),dest,dest_size);
#endif	/* CHECKED_BUILD */
		return false;
	}
	rv=regGetExistingValue("regGetString",value_key,reg_type_string);
	if(!rv)
		return false;
	res=rv->data_size<=dest_size;
	strCopy(dest,(char*)rv->data,dest_size);
	libvUnlockGlobal();
	return res;
}

vboolean	V_GLOBAL
regGetBinary(char *value_key,void *dest,u32_t dest_size)
{
	reg_value_t *rv;
	if(!value_key || !dest || !dest_size)
	{
#ifdef	CHECKED_BUILD
		conlPrintf(LOG_ERROR,
			"regGetBinary(%s,%X,%u): incorrect parameter(s).\n",
			strSafe(value_key),dest,dest_size);
#endif	/* CHECKED_BUILD */
		return false;
	}
	rv=regGetExistingValue("regGetBinary",value_key,reg_type_binary);
	if(!rv)
		return false;
	if(rv->data_size > dest_size)
	{
		libvUnlockGlobal();
#ifdef	CHECKED_BUILD
		conlPrintf(LOG_ERROR,"regGetBinary(%s,%X,%u): buffer must be "
			"at least %u bytes long.\n",strSafe(value_key),dest,dest_size,
			rv->data_size);
#endif	/* CHECKED_BUILD */
		return false;
	}
	memCopy(dest,rv->data,rv->data_size);
	libvUnlockGlobal();
	return true;
}

/*
 *	Stores new DWORD value in current section.
 */
vboolean	V_GLOBAL
regSetDword(char *value_key,u32_t value)
{
	if(!value_key)
	{
#ifdef	CHECKED_BUILD
		conlPrintf(LOG_ERROR,
			"regSetDword([NULL],%X): incorrect key name.\n",value);
#endif	/* CHECKED_BUILD */
		return false;
	}
	return regReplaceValue("regSetDword",value_key,
		reg_type_dword,&value,sizeof(u32_t));
}

vboolean	V_GLOBAL
regSetString(char *value_key,char *src)
{
	if(!value_key || !src)
	{
#ifdef	CHECKED_BUILD
		conlPrintf(LOG_ERROR,
			"regSetString(%s,%s): incorrect parameter(s).\n",
			strSafe(value_key),strSafe(src));
#endif	/* CHECKED_BUILD */
		return false;
	}
	return regReplaceValue("regSetString",value_key,
		reg_type_string,src,strlen(src)+1);
}

vboolean	V_GLOBAL
regSetBinary(char *value_key,void *src,u32_t src_size)
{
	if(!value_key || !src || !src_size)
	{
#ifdef	CHECKED_BUILD
		conlPrintf(LOG_ERROR,
			"regSetBinary(%s,%X,%u): incorrect parameter(s).\n",
			strSafe(value_key),src,src_size);
#endif	/* CHECKED_BUILD */
		return false;
	}
	return regReplaceValue("regSetBinary",value_key,
		reg_type_binary,src,src_size);
}

/*
 *	I/O
 */
vboolean	V_GLOBAL
regExport(char *file_name,reg_hive_t hive)
{
	vhandle_t out;
	global_data_t *gd;
	gd=libvLockGlobal();
	if(!gd->reg_root)
	{
		libvUnlockGlobal();
#ifdef	CHEKED_BUILD
		conlPrintf(LOG_ERROR,
			"regExport('%s'): registry not initialized.\n",strSafe(file_name));
#endif	/* CHEKED_BUILD */
		return false;
	}
	out=fileOpenWrite(file_name);
	if(!out)
	{
		libvUnlockGlobal();
#ifdef	CHEKED_BUILD
		conlPrintf(LOG_ERROR,
			"regExport('%s'): can't create output file.\n",strSafe(file_name));
#endif	/* CHEKED_BUILD */
		return false;
	}
	filePrintf(out,"REGEDIT4\n\n");
	switch(hive)
	{
		case reg_hive_local_user:
			regDumpSection_r(out,
				(ntree_t*)(gd->reg_root->children.buf[0]));
			break;
		case reg_hive_all_users:
			regDumpSection_r(out,
				(ntree_t*)(gd->reg_root->children.buf[1]));
			break;
	}
	libvUnlockGlobal();
	fileClose(out);
	return true;
}

char_p	V_LOCAL
reg_good_token(vhandle_t p,char *expected)
{
	if(!lexNext(p))
		return NULL;
	if(expected && strcmp(expected,lexLast(p)))
		return NULL;
	return lexLast(p);
}

/*
 *	I/O
 */
vboolean	V_GLOBAL
regImport(char *file_name)
{
	char *ptr;
	u32_t dw,len;
	vhandle_t fp,sp=NULL;
	global_data_t *gd;
	vboolean result;
	char name[_MAX_PATH];
	char value[_MAX_PATH];
	u8_stack_t bval;
	/* Initialize registry if not initialized */
	gd = libvLockGlobal();
	libvUnlockGlobal();
	if(!gd->reg_root)
		regCreate();
	/* Load input file */
	fp = lexParseFile(file_name,"");
	if(!fp)
	{
#ifdef	CHECKED_BUILD
		conlPrintf(LOG_ERROR,
			"regImport('%s'): can't parse file.\n",strSafe(file_name));
#endif	/* CHECKED_BUILD */
		return false;
	}
	/* Check signature */
	if(!reg_good_token(fp,"REGEDIT4"))
	{
#ifdef	CHECKED_BUILD
		conlPrintf(LOG_ERROR,"regImport('%s'): isn't valid "
			"registry file, wrong ID '%s'.\n",file_name,lexLast(fp));
#endif	/* CHECKED_BUILD */
		lexDestroy(fp);
		return false;
	}
	/* Parse all lines */
	u8sInit(&bval);
	lexEnable(fp,LEX_LINE_MODE);
	while(lexNext(fp))
	{
		len = strlen(lexLast(fp));
		if(!len)
			continue;
		ptr = &(lexLast(fp)[len-1]);
		while(*ptr<=32)
			ptr--;
		if(lexLast(fp)[0]=='[' && *ptr==']')
		{
			/* key name: keys always asolute */
			*ptr=0;
			lexLast(fp)[0]='\\';
			regSetKey(lexLast(fp));
			continue;
		}
		/* parse value */
		sp = lexParseString(lexLast(fp),"=:,");
		/* get name */
		if(!lexNext(sp))
			goto unexpected_eol;
		strCopy(name,lexLast(sp),_MAX_PATH);
		/* skip equal sign */
		if(!lexNext(sp))
			goto unexpected_eol;
		if(strcmp(lexLast(sp),"="))
		{
#ifdef		CHECKED_BUILD
			conlPrintf(LOG_ERROR,"regImport('%s'): parse error: "
				"expected '=' after '%s'.\n",
				file_name,name);
#endif		/* CHECKED_BUILD */
			break;
		}
		/* get next token */
		if(!lexNext(sp))
			goto unexpected_eol;
		strCopy(value,lexLast(sp),_MAX_PATH);
		/* is it typeid ? */
		if(!strcmp(value,"dword"))
		{
			if(!reg_good_token(sp,":"))
			{
#ifdef		CHECKED_BUILD
				conlPrintf(LOG_ERROR,"regImport('%s'): parse error: "
					"expected ':' after dword typeid.\n",
					file_name);
#endif		/* CHECKED_BUILD */
				break;
			}
			if(!lexNext(sp))
				goto unexpected_eol;
			if(!strToHex(lexLast(sp),&dw))
			{
#ifdef		CHECKED_BUILD
				conlPrintf(LOG_ERROR,"regImport('%s'): parse error: "
					"bad dword value '%s'.\n",
					file_name,lexLast(sp));
#endif		/* CHECKED_BUILD */
				break;
			}
			/* store dword in registry */
			regSetDword(name,dw);
		} else if(!strcmp(value,"hex"))
		{
			/* binary value */
			if(!reg_good_token(sp,":"))
			{
#ifdef		CHECKED_BUILD
				conlPrintf(LOG_ERROR,"regImport('%s'): parse error: "
					"expected ':' after binary typeid.\n",
					file_name);
#endif		/* CHECKED_BUILD */
				break;
			}
			while(sp && lexNext(sp))
			{
				if(lexLast(sp)[0]==',')
					continue;
				if(!strToHex(lexLast(sp),&dw))
				{
#ifdef				CHECKED_BUILD
					conlPrintf(LOG_ERROR,"regImport('%s'): parse error: "
						"bad binary value.\n",
						file_name);
#endif				/* CHECKED_BUILD */
					lexDestroy(sp);
					sp=NULL;	/* break inner loop */
					continue;
				}
				u8sPush(&bval,(u8_t)dw);
			}
			if(sp)	/* all parsed */
			{
				if(!u8sCount(&bval))
					goto unexpected_eol; /* only one comma in line ? */
				regSetBinary(name,u8sGetStorage(&bval),u8sCount(&bval));
			}
			u8sClear(&bval);
		} else {
			/* string value */
			regSetString(name,value);
		}
		if(sp)
		{
			lexDestroy(sp);
			sp=NULL;
		}
		continue;
unexpected_eol:;
#ifdef	CHECKED_BUILD
		lexSetInteger(sp,LEX_CURRENT_LINE,1);
		lexEnable(sp,LEX_LINE_MODE);
		conlPrintf(LOG_ERROR,
			"regImport('%s'): unexpected EOL in '%s'.\n",
			file_name,strSafe(lexNext(sp)));
#endif	/* CHECKED_BUILD */
		break;
	}
	if(sp)
		lexDestroy(sp);
	result = lexLast(fp) == NULL; /* all parsed ? */
	lexDestroy(fp);
	return result;
}
