/* This tool builds a cache of service specifications like the
   NeXTstep/ OPENSTEP 'make_services' tool.  In addition it builds a list of
   applications and services-bundles found in the standard directories.

   Copyright (C) 1998 Free Software Foundation, Inc.

   Written by:  Richard Frith-Macdonald <richard@brainstorm.co.uk>
   Created: November 1998

   This file is part of the GNUstep Project

   This program is free software; you can redistribute it and/or
   modify it under the terms of the GNU General Public License
   as published by the Free Software Foundation; either version 3
   of the License, or (at your option) any later version.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public
   License along with this library; see the file COPYING.
   If not, see <http://www.gnu.org/licenses/> or write to the
   Free Software Foundation, 51 Franklin Street, Fifth Floor,
   Boston, MA 02110-1301, USA.
*/

#include <stdlib.h>
#import <Foundation/NSArray.h>
#import <Foundation/NSBundle.h>
#import <Foundation/NSDictionary.h>
#import <Foundation/NSSet.h>
#import <Foundation/NSFileManager.h>
#import <Foundation/NSString.h>
#import <Foundation/NSProcessInfo.h>
#import <Foundation/NSData.h>
#import <Foundation/NSDebug.h>
#import <Foundation/NSDistributedLock.h>
#import <Foundation/NSAutoreleasePool.h>
#import <Foundation/NSPathUtilities.h>
#import <Foundation/NSSerialization.h>

static void scanApplications(NSMutableDictionary *services, NSString *path);
static void scanServices(NSMutableDictionary *services, NSString *path);
static void scanDynamic(NSMutableDictionary *services, NSString *path);
static NSMutableArray *validateEntry(id svcs, NSString* path);
static NSMutableDictionary *validateService(NSDictionary *service, NSString* path, unsigned i);

static NSString		*appsName = @".GNUstepAppList";
static NSString		*cacheName = @".GNUstepServices";

static	int verbose = 0;
static	NSMutableDictionary	*serviceMap;
static	NSMutableArray		*filterList;
static	NSMutableSet		*filterSet;
static	NSMutableDictionary	*printMap;
static	NSMutableDictionary	*spellMap;
static	NSMutableDictionary	*applicationMap;
static	NSMutableDictionary	*extensionsMap;
static	NSMutableDictionary	*schemesMap;

static Class aClass;
static Class dClass;
static Class sClass;

static BOOL CheckDirectory(NSString *path)
{
  NSFileManager	*mgr;
  BOOL		isDir;

  mgr = [NSFileManager defaultManager];
  if (([mgr fileExistsAtPath: path isDirectory: &isDir] && isDir) == 0)
    {
      NSString	*parent;

      parent = [path stringByDeletingLastPathComponent];
      if ([parent length] < [path length]
	&& CheckDirectory([parent stringByDeletingLastPathComponent]) == NO)
	{
	  return NO;
	}
      if ([mgr createDirectoryAtPath: path attributes: nil] == NO)
	{
	  return NO;
	}
    }
  return YES;
}

int
main(int argc, char** argv, char **env_c)
{
  NSAutoreleasePool	*pool;
  NSData		*data;
  NSProcessInfo		*proc;
  NSFileManager		*mgr;
  NSMutableDictionary	*services;
  NSArray		*args;
  NSString		*usrRoot;
  NSString		*str;
  unsigned		index;
  NSMutableDictionary	*fullMap;
  NSDictionary		*oldMap;
  NSEnumerator		*enumerator;
  NSString		*path;

#ifdef GS_PASS_ARGUMENTS
  [NSProcessInfo initializeWithArguments:argv count:argc environment:env_c];
#endif
  pool = [NSAutoreleasePool new];

  mgr = [NSFileManager defaultManager];

  aClass = [NSArray class];
  dClass = [NSDictionary class];
  sClass = [NSString class];

  proc = [NSProcessInfo processInfo];
  if (proc == nil)
    {
      NSLog(@"unable to get process information!");
      [pool release];
      exit(EXIT_SUCCESS);
    }

  [NSSerializer shouldBeCompact: YES];

  serviceMap = [NSMutableDictionary dictionaryWithCapacity: 64];
  filterList = [NSMutableArray arrayWithCapacity: 16];
  filterSet = [NSMutableSet setWithCapacity: 64];
  printMap = [NSMutableDictionary dictionaryWithCapacity: 8];
  spellMap = [NSMutableDictionary dictionaryWithCapacity: 8];
  applicationMap = [NSMutableDictionary dictionaryWithCapacity: 64];
  extensionsMap = [NSMutableDictionary dictionaryWithCapacity: 64];
  schemesMap = [NSMutableDictionary dictionaryWithCapacity: 64];

  args = [proc arguments];

  for (index = 1; index < [args count]; index++)
    {
      if ([[args objectAtIndex: index] isEqual: @"--verbose"])
	{
	  verbose++;
	}
      if ([[args objectAtIndex: index] isEqual: @"--quiet"])
	{
	  verbose--;
	}
      if ([[args objectAtIndex: index] isEqual: @"--help"])
	{
	  printf(
"make_services builds a validated cache of service information for use by\n"
"programs that want to use the OpenStep services facility.\n"
"This cache is stored in '%s' in the users GNUstep directory.\n"
"\n"
"You may use 'make_services --test filename' to test that the property list\n"
"in 'filename' contains a valid services definition.\n"
"You may use 'make_services --verbose' to produce descriptive output.\n"
"or --quiet to suppress any output (not recommended)\n",
[cacheName cString]);
	  exit(EXIT_SUCCESS);
	}
      if ([[args objectAtIndex: index] isEqual: @"--test"])
	{
	  verbose = YES;
	  while (++index < [args count])
	    {
	      NSString		*file = [args objectAtIndex: index];
	      NSDictionary	*info;

	      info = [NSDictionary dictionaryWithContentsOfFile: file];
	      if (info)
		{
		  id	svcs = [info objectForKey: @"NSServices"];

		  if (svcs)
		    {
		      validateEntry(svcs, file);
		    }
		  else if (verbose > 0)
		    {
		      NSLog(@"bad info - %@", file);
		    }
		}
	      else if (verbose > 0)
		{
		  NSLog(@"bad info - %@", file);
		}
	    }
	  exit(EXIT_SUCCESS);
	}
    }

  services = [NSMutableDictionary dictionaryWithCapacity: 200];

  /*
   *	Make sure that the users 'Services' directory exists.
   */
  usrRoot = [NSSearchPathForDirectoriesInDomains(NSLibraryDirectory,
    NSUserDomainMask, YES) lastObject];
  usrRoot = [usrRoot stringByAppendingPathComponent: @"Services"];
  if (CheckDirectory(usrRoot) == NO)
    {
      if (verbose > 0)
	NSLog(@"couldn't create %@", usrRoot);
      [pool release];
      exit(EXIT_FAILURE);
    }

  /*
   *	Before doing the main scan, we examine the 'Services' directory to
   *	see if any application has registered dynamic services - these take
   *	precedence over any listed in an applications Info_gnustep.plist.
   */
  scanDynamic(services, usrRoot);

  /*
   *	Scan for application information in all standard locations.
   */
  enumerator = [NSSearchPathForDirectoriesInDomains(
    NSAllApplicationsDirectory, NSAllDomainsMask, YES) objectEnumerator];
  while ((path = [enumerator nextObject]) != nil)
    {
      if ([path hasPrefix: @"."] == NO)
 	{
          scanApplications(services, path);
	}
    }

  /*
   *	Scan for service information in all standard locations.
   */
  enumerator = [NSSearchPathForDirectoriesInDomains(
    NSAllLibrariesDirectory, NSAllDomainsMask, YES) objectEnumerator];
  while ((path = [enumerator nextObject]) != nil)
    {
      scanServices(services,
	[path stringByAppendingPathComponent: @"Services"]);
    }

  fullMap = [NSMutableDictionary dictionaryWithCapacity: 5];
  [fullMap setObject: services forKey: @"ByPath"];
  [fullMap setObject: serviceMap forKey: @"ByService"];
  [fullMap setObject: filterList forKey: @"ByFilter"];
  [fullMap setObject: printMap forKey: @"ByPrint"];
  [fullMap setObject: spellMap forKey: @"BySpell"];

  str = [usrRoot stringByAppendingPathComponent: cacheName];
  if ([mgr fileExistsAtPath: str])
    {
      data = [NSData dataWithContentsOfFile: str];
      oldMap = [NSDeserializer deserializePropertyListFromData: data
					     mutableContainers: NO];
    }
  else
    {
      oldMap = nil;
    }
  if ([fullMap isEqual: oldMap] == NO)
    {
      data = [NSSerializer serializePropertyList: fullMap];
      if ([data writeToFile: str atomically: YES] == NO)
	{
	  if (verbose > 0)
	    NSLog(@"couldn't write %@", str);
	  [pool release];
	  exit(EXIT_FAILURE);
	}
    }

  str = [usrRoot stringByAppendingPathComponent: appsName];
  if ([mgr fileExistsAtPath: str])
    {
      data = [NSData dataWithContentsOfFile: str];
      oldMap = [NSDeserializer deserializePropertyListFromData: data
					     mutableContainers: NO];
    }
  else
    {
      oldMap = nil;
    }
  [applicationMap setObject: extensionsMap forKey: @"GSExtensionsMap"];
  [applicationMap setObject: schemesMap forKey: @"GSSchemesMap"];
  if ([applicationMap isEqual: oldMap] == NO)
    {
      data = [NSSerializer serializePropertyList: applicationMap];
      if ([data writeToFile: str atomically: YES] == NO)
	{
	  if (verbose > 0)
	    NSLog(@"couldn't write %@", str);
	  [pool release];
	  exit(EXIT_FAILURE);
	}
    }

  [pool release];
  exit(EXIT_SUCCESS);
}

/*
 * Load information about the shemes of URLs that an application supports.
 * For each scheme found, produce a dictionary, keyed by app name, that
 * contains dictionaries giving scheme info for that extension.
 * NB. in order to make schemes case-insensiteve - we always convert
 * to lowercase.
 */
static void addSchemesForApplication(NSDictionary *info, NSString *app)
{
  unsigned int  i;
  id            o0;
  NSArray       *a0;


  o0 = [info objectForKey: @"CFBundleURLTypes"];

  if (o0)
    {
      if ([o0 isKindOfClass: aClass] == NO)
        {
	  if (verbose > 0)
	    NSLog(@"bad app CFBundleURLTypes (not an array) - %@", app);
          return;
        }
      a0 = (NSArray*)o0;
      i = [a0 count];
      while (i-- > 0)
        {
          NSDictionary          *t;
          NSArray               *a1;
          id                    o1 = [a0 objectAtIndex: i];
          unsigned int          j;

          if ([o1 isKindOfClass: dClass] == NO)
            {
	      if (verbose > 0)
		NSLog(@"bad app CFBundleURLTypes (type not a dictionary) - %@",
		  app);
              return;
            }
	  /*
	   * Set 't' to the dictionary defining a particular file type.
	   */
          t = (NSDictionary*)o1;
	  o1 = [t objectForKey: @"CFBundleURLSchemes"];
          if (o1 == nil)
            {
              continue;
            }
          if ([o1 isKindOfClass: aClass] == NO)
            {
	      if (verbose > 0)
		NSLog(@"bad app CFBundleURLTypes (schemes not an array) - %@",
		  app);
              return;
            }
          a1 = (NSArray*)o1;
          j = [a1 count];
          while (j-- > 0)
            {
              NSString			*e;
              NSMutableDictionary	*d;

              e = [[a1 objectAtIndex: j] lowercaseString];
	      if ([e length] == 0)
		{
		  if (verbose > 0)
		    NSLog(@"Illegal (nul) scheme ignored for - %@", app);
		  return;
		}
              d = [schemesMap objectForKey: e];
              if (d == nil)
                {
                  d = [NSMutableDictionary dictionaryWithCapacity: 1];
                  [schemesMap setObject: d forKey: e];
                }
              if ([d objectForKey: app] == nil)
                {
                  [d setObject: t forKey: app];
                }
            }
        }
    }
}

/*
 * Load information about the types of files that an application supports.
 * For each extension found, produce a dictionary, keyed by app name, that
 * contains dictionaries giving type info for that extension.
 * NB. in order to make extensions case-insensiteve - we always convert
 * to lowercase.
 */
static void addExtensionsForApplication(NSDictionary *info, NSString *app)
{
  unsigned int  i;
  id            o0;
  NSArray       *a0;


  o0 = [info objectForKey: @"NSTypes"];
  if (o0 == nil)
    {
      o0 = [info objectForKey: @"CFBundleDocumentTypes"];
    }

  if (o0)
    {
      if ([o0 isKindOfClass: aClass] == NO)
        {
	  if (verbose > 0)
	    NSLog(@"bad app NSTypes (not an array) - %@", app);
          return;
        }
      a0 = (NSArray*)o0;
      i = [a0 count];
      while (i-- > 0)
        {
          NSDictionary          *t;
          NSArray               *a1;
          id                    o1 = [a0 objectAtIndex: i];
          unsigned int          j;

          if ([o1 isKindOfClass: dClass] == NO)
            {
	      if (verbose > 0)
		NSLog(@"bad app NSTypes (type not a dictionary) - %@", app);
              return;
            }
	  /*
	   * Set 't' to the dictionary defining a particular file type.
	   */
          t = (NSDictionary*)o1;
          o1 = [t objectForKey: @"NSUnixExtensions"];

	  if (o1 == nil)
	    {
	      o1 = [t objectForKey: @"CFBundleTypeExtensions"];
	    }
          if (o1 == nil)
            {
              continue;
            }
          if ([o1 isKindOfClass: aClass] == NO)
            {
	      if (verbose > 0)
		NSLog(@"bad app NSType (extensions not an array) - %@", app);
              return;
            }
          a1 = (NSArray*)o1;
          j = [a1 count];
          while (j-- > 0)
            {
              NSString			*e;
              NSMutableDictionary	*d;

              e = [[a1 objectAtIndex: j] lowercaseString];
	      if ([e length] == 0)
		{
		  if (verbose > 0)
		    NSLog(@"Illegal (nul) extension ignored for - %@", app);
		  return;
		}
              d = [extensionsMap objectForKey: e];
              if (d == nil)
                {
                  d = [NSMutableDictionary dictionaryWithCapacity: 1];
                  [extensionsMap setObject: d forKey: e];
                }
              if ([d objectForKey: app] == nil)
                {
                  [d setObject: t forKey: app];
                }
            }
        }
    }
  else
    {
      NSDictionary	*extensions;

      o0 = [info objectForKey: @"NSExtensions"];
      if (o0 == nil)
	{
	  o0 = [info objectForKey: @"CFBundleTypeExtensions"];
	}
      if (o0 == nil)
        {
          return;
        }
      if ([o0 isKindOfClass: dClass] == NO)
        {
	  if (verbose > 0)
	    NSLog(@"bad app NSExtensions (not a dictionary) - %@", app);
          return;
        }
      extensions = (NSDictionary *) o0;
      a0 = [extensions allKeys];
      i = [a0 count];
      while (i-- > 0)
        {
          id	tmp = [extensions objectForKey: [a0 objectAtIndex: i]];
          id	name;
          id	dict;

          if ([tmp isKindOfClass: dClass] == NO)
	    {
	      if (verbose > 0)
		NSLog(@"bad app NSExtensions (value isn't a dictionary) - %@",
		      app);
              continue;
	    }
          name = [[a0 objectAtIndex: i] lowercaseString];
          dict = [extensionsMap objectForKey: name];
          if (dict == nil)
	    {
              dict = [NSMutableDictionary dictionaryWithCapacity: 1];
	    }
          [dict setObject: tmp forKey: app];
          [extensionsMap setObject: dict forKey: name];
        }
    }
}

#if	0	// UNUSED
static void
scanDirectory(NSMutableDictionary *services, NSString *path)
{
  NSFileManager		*mgr = [NSFileManager defaultManager];
  NSAutoreleasePool	*arp = [NSAutoreleasePool new];
  NSArray		*contents = [mgr directoryContentsAtPath: path];
  unsigned		index;

  for (index = 0; index < [contents count]; index++)
    {
      NSString	*name = [contents objectAtIndex: index];
      NSString	*ext = [name pathExtension];
      NSString	*newPath;
      BOOL	isDir;

      /*
       *	Ignore anything with a leading dot.
       */
      if ([name hasPrefix: @"."] == YES)
 	{
	  continue;
	}
      if (ext != nil
	&& ([ext isEqualToString: @"app"] || [ext isEqualToString: @"debug"]
	|| [ext isEqualToString: @"profile"]))
	{
	  newPath = [path stringByAppendingPathComponent: name];
	  newPath = [newPath stringByStandardizingPath];
	  if ([mgr fileExistsAtPath: newPath isDirectory: &isDir] && isDir)
	    {
	      NSString		*oldPath;
	      NSBundle		*bundle;
	      NSDictionary	*info;

	      /*
	       *	All application paths are noted by name
	       *	in the 'applicationMap' dictionary.
	       */
              if ((oldPath = [applicationMap objectForKey: name]) == nil)
                {
                  [applicationMap setObject: newPath forKey: name];
                }
              else
                {
                  /*
                   * If we already have an entry for an application with
                   * this name, we skip this one - the first one takes
                   * precedence.
                   */
		  if (verbose > 0)
		    NSLog(@"duplicate app (%@) at '%@' and '%@'",
			  name, oldPath, newPath);
                  continue;
                }

	      bundle = [NSBundle bundleWithPath: newPath];
	      info = [bundle infoDictionary];
	      if (info)
		{
		  id	obj;

		  /*
		   * Load and validate any services definitions.
		   */
		  obj = [info objectForKey: @"NSServices"];
		  if (obj)
		    {
		      NSMutableArray	*entry;

		      entry = validateEntry(obj, newPath);
		      if (entry)
			{
			  [services setObject: entry forKey: newPath];
			}
		    }

		  addExtensionsForApplication(info, name);
		  addSchemesForApplication(info, name);
		}
	      else if (verbose > 0)
		{
		  NSLog(@"bad app info - %@", newPath);
		}
	    }
	  else if (verbose > 0)
	    {
	      NSLog(@"bad application - %@", newPath);
	    }
	}
      else if (ext != nil && [ext isEqualToString: @"service"])
	{
	  newPath = [path stringByAppendingPathComponent: name];
	  newPath = [newPath stringByStandardizingPath];
	  if ([mgr fileExistsAtPath: newPath isDirectory: &isDir] && isDir)
	    {
	      NSBundle		*bundle;
	      NSDictionary	*info;

	      bundle = [NSBundle bundleWithPath: newPath];
	      info = [bundle infoDictionary];
	      if (info)
		{
		  id	svcs = [info objectForKey: @"NSServices"];

		  if (svcs)
		    {
		      NSMutableArray	*entry;

		      entry = validateEntry(svcs, newPath);
		      if (entry)
			{
			  [services setObject: entry forKey: newPath];
			}
		    }
		  else if (verbose > 0)
		    {
		      NSLog(@"missing info - %@", newPath);
		    }
		}
	      else if (verbose > 0)
		{
		  NSLog(@"bad service info - %@", newPath);
		}
	    }
	  else if (verbose > 0)
	    {
	      NSLog(@"bad services bundle - %@", newPath);
	    }
	}
      else
	{
	  newPath = [path stringByAppendingPathComponent: name];
	  newPath = [newPath stringByStandardizingPath];
	  if ([mgr fileExistsAtPath: newPath isDirectory: &isDir] && isDir)
	    {
	      scanDirectory(services, newPath);
	    }
	}
    }
  [arp release];
}
#endif

static void
scanApplications(NSMutableDictionary *services, NSString *path)
{
  NSFileManager		*mgr = [NSFileManager defaultManager];
  NSAutoreleasePool	*arp = [NSAutoreleasePool new];
  NSArray		*contents = [mgr directoryContentsAtPath: path];
  unsigned		index;

  for (index = 0; index < [contents count]; index++)
    {
      NSString	*name = [contents objectAtIndex: index];
      NSString	*ext = [name pathExtension];
      NSString	*newPath;
      BOOL	isDir;

      /*
       *	Ignore anything with a leading dot.
       */
      if ([name hasPrefix: @"."] == YES)
 	{
	  continue;
	}
      if (ext != nil
	&& ([ext isEqualToString: @"app"] || [ext isEqualToString: @"debug"]
	|| [ext isEqualToString: @"profile"]))
	{
	  newPath = [path stringByAppendingPathComponent: name];
	  newPath = [newPath stringByStandardizingPath];
	  if ([mgr fileExistsAtPath: newPath isDirectory: &isDir] && isDir)
	    {
	      NSString		*oldPath;
	      NSBundle		*bundle;
	      NSDictionary	*info;

	      /*
	       *	All application paths are noted by name
	       *	in the 'applicationMap' dictionary.
	       */
              if ((oldPath = [applicationMap objectForKey: name]) == nil)
                {
                  [applicationMap setObject: newPath forKey: name];
                }
              else
                {
                  /*
                   * If we already have an entry for an application with
                   * this name, we skip this one - the first one takes
                   * precedence.
                   */
		  if (verbose > 0)
		    NSLog(@"duplicate app (%@) at '%@' and '%@'",
			  name, oldPath, newPath);
                  continue;
                }

	      bundle = [NSBundle bundleWithPath: newPath];
	      info = [bundle infoDictionary];
	      if (info)
		{
		  id	obj;

		  /*
		   * Load and validate any services definitions.
		   */
		  obj = [info objectForKey: @"NSServices"];
		  if (obj)
		    {
		      NSMutableArray	*entry;

		      entry = validateEntry(obj, newPath);
		      if (entry)
			{
			  [services setObject: entry forKey: newPath];
			}
		    }

		  addExtensionsForApplication(info, name);
		  addSchemesForApplication(info, name);
		}
	      else if (verbose > 0)
		{
		  NSLog(@"bad app info - %@", newPath);
		}
	    }
	  else if (verbose > 0)
	    {
	      NSLog(@"bad application - %@", newPath);
	    }
	}
      else
	{
	  newPath = [path stringByAppendingPathComponent: name];
	  newPath = [newPath stringByStandardizingPath];
	  if ([mgr fileExistsAtPath: newPath isDirectory: &isDir] && isDir)
	    {
	      scanApplications(services, newPath);
	    }
	}
    }
  [arp release];
}

static void
scanDynamic(NSMutableDictionary *services, NSString *path)
{
  NSFileManager		*mgr = [NSFileManager defaultManager];
  NSAutoreleasePool	*arp = [NSAutoreleasePool new];
  NSArray		*contents = [mgr directoryContentsAtPath: path];
  unsigned		index;

  for (index = 0; index < [contents count]; index++)
    {
      NSString		*name = [contents objectAtIndex: index];
      NSString		*infPath;
      NSDictionary	*info;

      /*
       *	Ignore anything with a leading dot.
       */
      if ([name hasPrefix: @"."])
	{
	  continue;
	}

      /* *.service bundles are handled in scanDirectory */
      if ([[name pathExtension] isEqualToString: @"service"])
	continue;

      infPath = [path stringByAppendingPathComponent: name];

      info = [NSDictionary dictionaryWithContentsOfFile: infPath];
      if (info)
	{
	  id	svcs = [info objectForKey: @"NSServices"];

	  if (svcs)
	    {
	      NSMutableArray	*entry;

	      entry = validateEntry(svcs, infPath);
	      if (entry)
		{
		  [services setObject: entry forKey: infPath];
		}
	    }
	}
      else if (verbose > 0)
	{
	  NSLog(@"bad app info - %@", infPath);
	}
    }
  [arp release];
}

static void
scanServices(NSMutableDictionary *services, NSString *path)
{
  NSFileManager		*mgr = [NSFileManager defaultManager];
  NSAutoreleasePool	*arp = [NSAutoreleasePool new];
  NSArray		*contents = [mgr directoryContentsAtPath: path];
  unsigned		index;

  for (index = 0; index < [contents count]; index++)
    {
      NSString	*name = [contents objectAtIndex: index];
      NSString	*ext = [name pathExtension];
      NSString	*newPath;
      BOOL	isDir;

      /*
       *	Ignore anything with a leading dot.
       */
      if ([name hasPrefix: @"."] == YES)
 	{
	  continue;
	}
      if (ext != nil && [ext isEqualToString: @"service"])
	{
	  newPath = [path stringByAppendingPathComponent: name];
	  newPath = [newPath stringByStandardizingPath];
	  if ([mgr fileExistsAtPath: newPath isDirectory: &isDir] && isDir)
	    {
	      NSBundle		*bundle;
	      NSDictionary	*info;

	      bundle = [NSBundle bundleWithPath: newPath];
	      info = [bundle infoDictionary];
	      if (info)
		{
		  id	svcs = [info objectForKey: @"NSServices"];

		  if (svcs)
		    {
		      NSMutableArray	*entry;

		      entry = validateEntry(svcs, newPath);
		      if (entry)
			{
			  [services setObject: entry forKey: newPath];
			}
		    }
		  else if (verbose > 0)
		    {
		      NSLog(@"missing info - %@", newPath);
		    }
		}
	      else if (verbose > 0)
		{
		  NSLog(@"bad service info - %@", newPath);
		}
	    }
	  else if (verbose > 0)
	    {
	      NSLog(@"bad services bundle - %@", newPath);
	    }
	}
      else
	{
	  newPath = [path stringByAppendingPathComponent: name];
	  newPath = [newPath stringByStandardizingPath];
	  if ([mgr fileExistsAtPath: newPath isDirectory: &isDir] && isDir)
	    {
	      scanServices(services, newPath);
	    }
	}
    }
  [arp release];
}

static NSMutableArray*
validateEntry(id svcs, NSString *path)
{
  NSMutableArray	*newServices;
  NSArray		*services;
  unsigned		pos;

  if ([svcs isKindOfClass: aClass] == NO)
    {
      if (verbose > 0)
	NSLog(@"NSServices entry not an array - %@", path);
      return nil;
    }

  services = (NSArray*)svcs;
  newServices = [NSMutableArray arrayWithCapacity: [services count]];
  for (pos = 0; pos < [services count]; pos++)
    {
      id			svc;

      svc = [services objectAtIndex: pos];
      if ([svc isKindOfClass: dClass])
	{
	  NSDictionary		*service = (NSDictionary*)svc;
	  NSMutableDictionary	*newService;

	  newService = validateService(service, path, pos);
	  if (newService)
	    {
	      [newServices addObject: newService];
	    }
	}
      else if (verbose > 0)
	{
	  NSLog(@"NSServices entry %u not a dictionary - %@",
	    pos, path);
	}
    }
  return newServices;
}

static NSMutableDictionary*
validateService(NSDictionary *service, NSString *path, unsigned pos)
{
  static NSDictionary	*fields = nil;
  NSEnumerator		*e;
  NSMutableDictionary	*result;
  NSString		*k;
  id			obj;

  if (fields == nil)
    {
      fields = [NSDictionary dictionaryWithObjectsAndKeys:
	@"string", @"NSMessage",
	@"string", @"NSPortName",
	@"array", @"NSSendTypes",
	@"array", @"NSReturnTypes",
	@"dictionary", @"NSMenuItem",
	@"dictionary", @"NSKeyEquivalent",
	@"string", @"NSUserData",
	@"string", @"NSTimeout",
	@"string", @"NSHost",
	@"string", @"NSExecutable",
	@"string", @"NSFilter",
	@"string", @"NSInputMechanism",
	@"string", @"NSPrintFilter",
	@"string", @"NSDeviceDependent",
	@"array", @"NSLanguages",
	@"string", @"NSSpellChecker",
	nil];
      [fields retain];
    }

  result = [NSMutableDictionary dictionaryWithCapacity: [service count]];

  /*
   *	Step through and check that each field is a known one and of the
   *	correct type.
   */
  e = [service keyEnumerator];
  while ((k = [e nextObject]) != nil)
    {
      NSString	*type = [fields objectForKey: k];

      if (type == nil)
	{
	  if (verbose > 0)
	    NSLog(@"NSServices entry %u spurious field (%@)- %@", pos, k, path);
	}
      else
	{
	  obj = [service objectForKey: k];
	  if ([type isEqualToString: @"string"])
	    {
	      if ([obj isKindOfClass: sClass] == NO)
		{
		  if (verbose > 0)
		    NSLog(@"NSServices entry %u field %@ is not a string "
			  @"- %@", pos, k, path);
		  return nil;
		}
	      [result setObject: obj forKey: k];
	    }
	  else if ([type isEqualToString: @"array"])
	    {
	      NSArray	*a;

	      if ([obj isKindOfClass: aClass] == NO)
		{
		  if (verbose > 0)
		    NSLog(@"NSServices entry %u field %@ is not an array "
		    @"- %@", pos, k, path);
		  return nil;
		}
	      a = (NSArray*)obj;
	      if ([a count] == 0)
		{
		  if (verbose > 0)
		    NSLog(@"NSServices entry %u field %@ is an empty array "
			  @"- %@", pos, k, path);
		}
	      else
		{
		  unsigned	i;

		  for (i = 0; i < [a count]; i++)
		    {
		      if ([[a objectAtIndex: i] isKindOfClass: sClass] == NO)
			{
			  if (verbose > 0)
			    NSLog(@"NSServices entry %u field %@ element %u is "
				  @"not a string - %@", pos, k, i, path);
			  return nil;
			}
		    }
		  [result setObject: a forKey: k];
		}
	    }
	  else if ([type isEqualToString: @"dictionary"])
	    {
	      NSDictionary	*d;

	      if ([obj isKindOfClass: dClass] == NO)
		{
		  if (verbose > 0)
		    NSLog(@"NSServices entry %u field %@ is not a dictionary "
			  @"- %@", pos, k, path);
		  return nil;
		}
	      d = (NSDictionary*)obj;
	      if ([d objectForKey: @"default"] == nil)
		{
		  if (verbose > 0)
		    NSLog(@"NSServices entry %u field %@ has no default value "
			  @"- %@", pos, k, path);
		}
	      else
		{
		  NSEnumerator	*e = [d objectEnumerator];

		  while ((obj = [e nextObject]) != nil)
		    {
		      if ([obj isKindOfClass: sClass] == NO)
			{
			  if (verbose > 0)
			    NSLog(@"NSServices entry %u field %@ contains "
				  @"non-string value - %@", pos, k, path);
			  return nil;
			}
		    }
		}
	      [result setObject: d forKey: k];
	    }
	}
    }

  /*
   *	Record in this service dictionary where it is to be found.
   */
  [result setObject: path forKey: @"ServicePath"];

  /*
   *	Now check that we have the required fields for the service.
   */
  if ((obj = [result objectForKey: @"NSFilter"]) != nil)
    {
      NSString		*str;
      NSArray		*snd;
      NSArray		*ret;
      BOOL		notPresent = NO;

      str = [result objectForKey: @"NSInputMechanism"];
      if (str != nil)
	{
	  if ([str isEqualToString: @"NSUnixStdio"] == NO
	    && [str isEqualToString: @"NSMapFile"] == NO
	    && [str isEqualToString: @"NSIdentity"] == NO)
	  {
	    if (verbose > 0)
	      NSLog(@"NSServices entry %u bad input mechanism - %@", pos, path);
	    return nil;
	  }
	}
      else if ([result objectForKey: @"NSPortName"] == nil)
	{
	  if (verbose > 0)
	    NSLog(@"NSServices entry %u NSPortName missing - %@", pos, path);
	  return nil;
	}

      snd = [result objectForKey: @"NSSendTypes"];
      ret = [result objectForKey: @"NSReturnTypes"];
      if ([snd count] == 0 || [ret count] == 0)
	{
	  if (verbose > 0)
	    NSLog(@"NSServices entry %u types empty or missing - %@",
	      pos, path);
	  return nil;
	}
      else
	{
	  unsigned	i = [snd count];

	  /*
	   * See if this filter handles any send/return combination
	   * which is not alreadly present.
	   */
	  while (notPresent == NO && i-- > 0)
	    {
	      unsigned	j = [ret count];

	      while (notPresent == NO && j-- > 0)
		{
		  str = [NSString stringWithFormat: @"%@==>%@",
		    [snd objectAtIndex: i], [ret objectAtIndex: j]];
		  if ([filterSet member: str] == nil)
		    {
		      notPresent = YES;
		      [filterSet addObject: str];
		      [filterList addObject: result];
		    }
		}
	    }
	}
      if (notPresent == NO)
	{
	  if (verbose > 0)
	    {
	      NSLog(@"Ignoring duplicate %u in %@ -\n%@", pos, path, result);
	    }
	  return nil;
	}
    }
  else if ((obj = [result objectForKey: @"NSMessage"]) != nil)
    {
      NSDictionary	*item;
      NSEnumerator	*e;
      NSString		*k;
      BOOL		used = NO;

      if ([result objectForKey: @"NSPortName"] == nil)
	{
	  if (verbose > 0)
	    NSLog(@"NSServices entry %u NSPortName missing - %@", pos, path);
	  return nil;
	}
      if ([result objectForKey: @"NSSendTypes"] == nil
	&& [result objectForKey: @"NSReturnTypes"] == nil)
	{
	  if (verbose > 0)
	    NSLog(@"NSServices entry %u types missing - %@", pos, path);
	  return nil;
	}
      if ((item = [result objectForKey: @"NSMenuItem"]) == nil)
	{
	  if (verbose > 0)
	    NSLog(@"NSServices entry %u NSMenuItem missing - %@", pos, path);
	  return nil;
	}

      /*
       *	For each language, check to see if we already have a service
       *	by this name - if so - we ignore this one.
       */
      e = [item keyEnumerator];
      while ((k = [e nextObject]) != nil)
	{
	  NSString		*name = [item objectForKey: k];
	  NSMutableDictionary	*names;

	  names = [serviceMap objectForKey: k];
	  if (names == nil)
	    {
	      names = [NSMutableDictionary dictionaryWithCapacity: 1];
	      [serviceMap setObject: names forKey: k];
	    }
	  if ([names objectForKey: name] == nil)
	    {
	      [names setObject: result forKey: name];
	      used = YES;
	    }
	}
      if (used == NO)
	{
	  if (verbose > 0)
	    {
	      NSLog(@"Ignoring entry %u in %@ -\n%@", pos, path, result);
	    }
	  return nil;	/* Ignore - already got service with this name	*/
	}
    }
  else if ((obj = [result objectForKey: @"NSPrintFilter"]) != nil)
    {
      NSDictionary	*item;
      NSEnumerator	*e;
      NSString		*k;
      BOOL		used = NO;

      if ((item = [result objectForKey: @"NSMenuItem"]) == nil)
	{
	  if (verbose > 0)
	    NSLog(@"NSServices entry %u NSMenuItem missing - %@", pos, path);
	  return nil;
	}
      /*
       *	For each language, check to see if we already have a print
       *	filter by this name - if so - we ignore this one.
       */
      e = [item keyEnumerator];
      while ((k = [e nextObject]) != nil)
	{
	  NSString		*name = [item objectForKey: k];
	  NSMutableDictionary	*names;

	  names = [printMap objectForKey: k];
	  if (names == nil)
	    {
	      names = [NSMutableDictionary dictionaryWithCapacity: 1];
	      [printMap setObject: names forKey: k];
	    }
	  if ([names objectForKey: name] == nil)
	    {
	      [names setObject: result forKey: name];
	      used = YES;
	    }
	}
      if (used == NO)
	{
	  if (verbose > 0)
	    {
	      NSLog(@"Ignoring entry %u in %@ -\n%@", pos, path, result);
	    }
	  return nil;	/* Ignore - already got filter with this name	*/
	}
    }
  else if ((obj = [result objectForKey: @"NSSpellChecker"]) != nil)
    {
      NSArray	*item;
      unsigned	pos;
      BOOL	used = NO;

      if ((item = [result objectForKey: @"NSLanguages"]) == nil)
	{
	  if (verbose > 0)
	    NSLog(@"NSServices entry NSLanguages missing - %@", path);
	  return nil;
	}
      /*
       *	For each language, check to see if we already have a spell
       *	checker by this name - if so - we ignore this one.
       */
      pos = [item count];
      while (pos-- > 0)
	{
	  NSString	*lang = [item objectAtIndex: pos];

	  if ([spellMap objectForKey: lang] == nil)
	    {
	      [spellMap setObject: result forKey: lang];
	      used = YES;
	    }
	}
      if (used == NO)
	{
	  if (verbose > 0)
	    {
	      NSLog(@"Ignoring entry %u in %@ -\n%@", pos, path, result);
	    }
	  return nil;	/* Ignore - already got speller with language.	*/
	}
    }
  else
    {
      if (verbose > 0)
	NSLog(@"NSServices entry %u unknown service/filter - %@", pos, path);
      return nil;
    }

  return result;
}

