/****************************************************************************
 * fs/fat/fs_fat32dirent.c
 *
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.  The
 * ASF licenses this file to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance with the
 * License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
 * License for the specific language governing permissions and limitations
 * under the License.
 *
 ****************************************************************************/

/****************************************************************************
 * NOTE:  If CONFIG_FAT_LFN is defined, then there may be some legal, patent
 * issues. The following was extracted from the entry "File Allocation Table
 * from Wikipedia, the free encyclopedia:
 *
 * "On December 3, 2003 Microsoft announced it would be offering licenses
 *  for use of its FAT specification and 'associated intellectual property',
 *  at the cost of a US$0.25 royalty per unit sold, with a $250,000 maximum
 *  royalty per license agreement.
 *
 *  o "U.S. Patent 5,745,902 (http://www.google.com/patents?vid=5745902) -
 *     Method and system for accessing a file using file names having
 *     different file name formats. ...
 *  o "U.S. Patent 5,579,517 (http://www.google.com/patents?vid=5579517) -
 *     Common name space for long and short filenames. ...
 *  o "U.S. Patent 5,758,352 (http://www.google.com/patents?vid=5758352) -
 *     Common name space for long and short filenames. ...
 *  o "U.S. Patent 6,286,013 (http://www.google.com/patents?vid=6286013) -
 *     Method and system for providing a common name space for long and
 *     short file names in an operating system. ...
 *
 * "Many technical commentators have concluded that these patents only cover
 *  FAT implementations that include support for long filenames, and that
 *  removable solid state media and consumer devices only using short names
 *  would be unaffected. ..."
 *
 * So you have been forewarned:  Use the long filename at your own risk!
 *
 ****************************************************************************/

/****************************************************************************
 * Included Files
 ****************************************************************************/

#include <nuttx/config.h>

#include <sys/param.h>
#include <sys/types.h>

#include <stdint.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <assert.h>
#include <errno.h>
#include <debug.h>

#include <nuttx/fs/fs.h>
#include <nuttx/fs/fat.h>

#include "inode/inode.h"
#include "fs_fat32.h"

/****************************************************************************
 * Private Types
 ****************************************************************************/

enum fat_case_e
{
  FATCASE_UNKNOWN = 0,
  FATCASE_UPPER,
  FATCASE_LOWER
};

/****************************************************************************
 * Private Function Prototypes
 ****************************************************************************/

#ifdef CONFIG_FAT_LFN
static uint8_t fat_lfnchecksum(FAR const uint8_t *sfname);
#endif
static inline int fat_parsesfname(FAR const char **path,
                                  FAR struct fat_dirinfo_s *dirinfo,
                                  FAR char *terminator);
#ifdef CONFIG_FAT_LFN
static inline int fat_parselfname(FAR const char **path,
                                  FAR struct fat_dirinfo_s *dirinfo,
                                  FAR char *terminator);
static inline int fat_createalias(FAR struct fat_dirinfo_s *dirinfo);
static inline int fat_findalias(FAR struct fat_mountpt_s *fs,
                                FAR struct fat_dirinfo_s *dirinfo);
static inline int fat_uniquealias(FAR struct fat_mountpt_s *fs,
                                  FAR struct fat_dirinfo_s *dirinfo);
#endif
static int fat_path2dirname(FAR const char **path,
                            FAR struct fat_dirinfo_s *dirinfo,
                            FAR char *terminator);
static int fat_findsfnentry(FAR struct fat_mountpt_s *fs,
                            FAR struct fat_dirinfo_s *dirinfo);
#ifdef CONFIG_FAT_LFN
static bool fat_cmplfnchunk(FAR uint8_t *chunk, FAR const lfnchar *substr,
                            int nchunk);
static bool fat_cmplfname(FAR const uint8_t *direntry,
                          FAR const lfnchar *substr);
static inline int fat_findlfnentry(FAR struct fat_mountpt_s *fs,
                                   FAR struct fat_dirinfo_s *dirinfo);

#endif
static inline int fat_allocatesfnentry(FAR struct fat_mountpt_s *fs,
                                       FAR struct fat_dirinfo_s *dirinfo);
#ifdef CONFIG_FAT_LFN
static inline int fat_allocatelfnentry(FAR struct fat_mountpt_s *fs,
                                       FAR struct fat_dirinfo_s *dirinfo);
#endif
static inline int fat_getsfname(FAR uint8_t *direntry, FAR char *buffer,
                                unsigned int buflen);
#ifdef CONFIG_FAT_LFN
static void fat_getlfnchunk(FAR uint8_t *chunk, FAR lfnchar *dest,
                            int nchunk);
static inline int fat_getlfname(FAR struct fat_mountpt_s *fs,
                                FAR struct fs_dirent_s *dir,
                                FAR struct dirent *entry);
#endif
static int fat_putsfname(FAR struct fat_mountpt_s *fs,
                         FAR struct fat_dirinfo_s *dirinfo);
#ifdef CONFIG_FAT_LFN
static void fat_initlfname(FAR uint8_t *chunk, int nchunk);
static void fat_putlfnchunk(FAR uint8_t *chunk, FAR const lfnchar *src,
                            int nchunk);
static int fat_putlfname(FAR struct fat_mountpt_s *fs,
                         FAR struct fat_dirinfo_s *dirinfo);
#endif
static int fat_putsfdirentry(FAR struct fat_mountpt_s *fs,
                             FAR struct fat_dirinfo_s *dirinfo,
                             uint8_t attributes, uint32_t fattime);

#if defined(CONFIG_FAT_LFN) && defined(CONFIG_FAT_LFN_UTF8)
static int fat_utf8toucs(FAR const char **str, FAR lfnchar *ucs);
static int fat_ucstoutf8(FAR uint8_t *dest, uint8_t offset, lfnchar ucs);
#endif

/****************************************************************************
 * Private Functions
 ****************************************************************************/

/****************************************************************************
 * Name: fat_utf8toucs
 *
 * Description:
 *   Convert the next characters from UTF8 to UCS2.
 *
 ****************************************************************************/
#if defined(CONFIG_FAT_LFN) && defined(CONFIG_FAT_LFN_UTF8)
static int fat_utf8toucs(FAR const char **str, FAR lfnchar *ucs)
{
  uint8_t chr;
  lfnchar tucs;
  int ret = ERROR;

  *ucs = '\0';
  chr = *((*str)++);

  if ((chr & 0x80) == 0x00)
    {
      tucs = (lfnchar)chr;
      ret = OK;
    }
  else if ((chr & 0xe0) == 0xc0)
    {
      tucs = ((lfnchar)(chr & ~0xe0)) << 6;
      chr = *((*str)++);
      if ((chr & 0xc0) == 0x80)
        {
          tucs |= (lfnchar)(chr & ~0xc0);
          ret = OK;
        }
    }
  else if ((chr & 0xf0) == 0xe0)
    {
      tucs = ((lfnchar)(chr & ~0xf0)) << 12;
      chr = *((*str)++);
      if ((chr & 0xc0) == 0x80)
        {
          tucs |= (lfnchar)(chr & ~0xc0) << 6;
          chr = *((*str)++);
          if ((chr & 0xc0) == 0x80)
            {
              tucs |= (lfnchar)(chr & ~0xc0);
              ret = OK;
            }
        }
    }

  if (ret == OK)
    {
      *ucs = tucs;
    }

  return ret;
}
#endif

/****************************************************************************
 * Name: fat_utf8toucs
 *
 * Description:
 *   Convert the next character from UCS2 to UTF8, reverse.
 *
 ****************************************************************************/
#if defined(CONFIG_FAT_LFN) && defined(CONFIG_FAT_LFN_UTF8)
static int fat_ucstoutf8(FAR uint8_t *dest, uint8_t offset, lfnchar ucs)
{
  if (ucs < 128 && offset >= 1)
    {
      dest[--offset] = (uint8_t)(ucs & 0xff);
    }
  else if (ucs < 2048 && offset >= 2)
    {
      dest[--offset] = (uint8_t)((ucs >>  0) & ~0xc0) | 0x80;
      dest[--offset] = (uint8_t)((ucs >>  6) & ~0xe0) | 0xc0;
    }
  else if (offset >= 3)
    {
      dest[--offset] = (uint8_t)((ucs >>  0) & ~0xc0) | 0x80;
      dest[--offset] = (uint8_t)((ucs >>  6) & ~0xc0) | 0x80;
      dest[--offset] = (uint8_t)((ucs >> 12) & ~0xf0) | 0xe0;
    }

  return offset;
}
#endif

/****************************************************************************
 * Name: fat_lfnchecksum
 *
 * Description:
 *   Verify that the checksum of the short file name matches the checksum
 *   that we found in the long file name entries.
 *
 ****************************************************************************/

#ifdef CONFIG_FAT_LFN
static uint8_t fat_lfnchecksum(FAR const uint8_t *sfname)
{
  uint8_t sum = 0;
  int i;

  for (i = DIR_MAXFNAME; i; i--)
    {
      sum = ((sum & 1) << 7) + (sum >> 1) + *sfname++;
    }

  return sum;
}
#endif

/****************************************************************************
 * Name: fat_parsesfname
 *
 * Description:  Convert a user filename into a properly formatted FAT
 *   (short 8.3) filename as it would appear in a directory entry.  Here are
 *    the rules for the 8+3 short file name in the directory:
 *
 *   The first byte:
 *
 *     0xe5 = The directory is free
 *     0x00 = This directory and all following directories are free
 *     0x05 = Really 0xe5
 *     0x20 = May NOT be ' '
 *
 *   Other characters may be any characters except for the following:
 *
 *     0x00-0x1f = (except for 0x00 and 0x05 in the first byte)
 *     0x22      = '"'
 *     0x2a-0x2c = '*', '+', ','
 *     0x2e-0x2f = '.', '/'
 *     0x3a-0x3f = ':', ';', '<', '=', '>', '?'
 *     0x5b-0x5d = '[', '\\', ']'
 *     0x7c      = '|'
 *
 *   '.' May only occur once within string and only within the first 9
 *   bytes.  The '.' is not save in the directory, but is implicit in
 *   8+3 format.
 *
 *   Lower case characters are not allowed in directory names (without some
 *   poorly documented operations on the NTRes directory byte).  Lower case
 *   codes may represent different characters in other character sets ("DOS
 *   code pages").  The logic below does not, at present, support any other
 *   character sets.
 *
 * Returned Value:
 *   OK - The path refers to a valid 8.3 FAT file name and has been properly
 *        converted and stored in dirinfo.
 *   <0 - Otherwise an negated error is returned meaning that the string is
 *        not a valid 8+3 because:
 *
 *        1. Contains characters not in the printable character set,
 *        2. Contains forbidden characters or multiple '.' characters
 *        3. File name or extension is too long.
 *
 *        If CONFIG_FAT_LFN is defined and CONFIG_FAT_LCNAMES is NOT
 *        defined, then:
 *
 *        4a. File name or extension contains lower case characters.
 *
 *        If CONFIG_FAT_LFN is defined and CONFIG_FAT_LCNAMES is defined,
 *        then:
 *
 *        4b. File name or extension is not all the same case.
 *
 ****************************************************************************/

static inline int fat_parsesfname(FAR const char **path,
                                  FAR struct fat_dirinfo_s *dirinfo,
                                  FAR char *terminator)
{
#ifdef CONFIG_FAT_LCNAMES
  unsigned int ntlcenable = FATNTRES_LCNAME | FATNTRES_LCEXT;
  unsigned int ntlcfound  = 0;
#ifdef CONFIG_FAT_LFN
  enum fat_case_e namecase = FATCASE_UNKNOWN;
  enum fat_case_e extcase  = FATCASE_UNKNOWN;
#endif
#endif
  FAR const char *node = *path;
  int endndx;
  uint8_t ch;
  int ndx = 0;

  /* Initialized the name with all spaces */

  memset(dirinfo->fd_name, ' ', DIR_MAXFNAME);

  /* Loop until the name is successfully parsed or an error occurs */

  endndx  = 8;
  for (; ; )
    {
      /* Get the next byte from the path */

      ch = *node++;

      /* Check if this the last byte in this node of the name */

      if ((ch == '\0' || ch == '/') && ndx != 0)
        {
          /* Return the accumulated NT flags and the terminating character */

#ifdef CONFIG_FAT_LCNAMES
          dirinfo->fd_ntflags = ntlcfound & ntlcenable;
#endif
          *terminator = ch;
          *path       = node;
          return OK;
        }

      /* Accept only the printable character set (excluding space).  Note
       * that the first byte of the name could be 0x05 meaning that is it
       * 0xe5, but this is not a printable character in this character in
       * either case.
       */

      else if (!isgraph(ch))
        {
          goto errout;
        }

      /* Check for transition from name to extension.  Only one '.' is
       * permitted and it must be within first 9 characters
       */

      else if (ch == '.' && endndx == 8)
        {
          /* Starting the extension */

          ndx    = 8;
          endndx = 11;
          continue;
        }

      /* Reject printable characters forbidden by FAT */

      else if (ch == '"'  ||  (ch >= '*' && ch <= ',') ||
               ch == '.'  ||   ch == '/'               ||
              (ch >= ':'  &&   ch <= '?')              ||
              (ch >= '['  &&   ch <= ']')              ||
              (ch == '|'))
        {
          goto errout;
        }

      /* Check for upper case characters */

#ifdef CONFIG_FAT_LCNAMES
      else if (isupper(ch))
        {
          /* Some or all of the characters in the name or extension
           * are upper case. Force all of the characters to be interpreted
           * as upper case.
           */

          if (endndx == 8)
            {
              /* Is there mixed case in the name? */

#ifdef CONFIG_FAT_LFN
              if (namecase == FATCASE_LOWER)
                {
                  /* Mixed case in the name -- use the long file name */

                  goto errout;
                }

              /* So far, only upper case in the name */

              namecase = FATCASE_UPPER;
#endif

              /* Clear lower case name bit in mask */

              ntlcenable &= ~FATNTRES_LCNAME;
            }
          else
            {
              /* Is there mixed case in the extension? */

#ifdef CONFIG_FAT_LFN
              if (extcase == FATCASE_LOWER)
                {
                  /* Mixed case in the extension -- use the long file name */

                  goto errout;
                }

              /* So far, only upper case in the extension */

              extcase = FATCASE_UPPER;
#endif

              /* Clear lower case extension in mask */

              ntlcenable &= ~FATNTRES_LCEXT;
            }
        }
#endif

      /* Check for lower case characters */

      else if (islower(ch))
        {
#if defined(CONFIG_FAT_LFN) && !defined(CONFIG_FAT_LCNAMES)
          /* If lower case characters are present, then a long file
           * name will be constructed.
           */

          goto errout;
#else
          /* Convert the character to upper case */

          ch = toupper(ch);

          /* Some or all of the characters in the name or extension
           * are lower case.  They can be interpreted as lower case if
           * only if all of the characters in the name or extension are
           * lower case.
           */

#ifdef CONFIG_FAT_LCNAMES
          if (endndx == 8)
            {
              /* Is there mixed case in the name? */

#ifdef CONFIG_FAT_LFN
              if (namecase == FATCASE_UPPER)
                {
                  /* Mixed case in the name -- use the long file name */

                  goto errout;
                }

              /* So far, only lower case in the name */

              namecase = FATCASE_LOWER;
#endif

              /* Set lower case name bit */

              ntlcfound |= FATNTRES_LCNAME;
            }
          else
            {
              /* Is there mixed case in the extension? */

#ifdef CONFIG_FAT_LFN
              if (extcase == FATCASE_UPPER)
                {
                  /* Mixed case in the extension -- use the long file name */

                  goto errout;
                }

              /* So far, only lower case in the extension */

              extcase = FATCASE_LOWER;
#endif

              /* Set lower case extension bit */

              ntlcfound |= FATNTRES_LCEXT;
            }
#endif
#endif /* CONFIG_FAT_LFN && !CONFIG_FAT_LCNAMES */
        }

      /* Check if the file name exceeds the size permitted (without
       * long file name support).
       */

      if (ndx >= endndx)
        {
          goto errout;
        }

      /* Save next character in the accumulated name */

      dirinfo->fd_name[ndx++] = ch;
    }

errout:
  return -EINVAL;
}

/****************************************************************************
 * Name: fat_parselfname
 *
 * Description:  Convert a user filename into a properly formatted FAT
 *   long filename as it would appear in a directory entry.  Here are
 *   the rules for the long file name in the directory:
 *
 *   Valid characters are the same as for short file names EXCEPT:
 *
 *     1. '+', ',', ';', '=', '[', and ']' are accepted in the file name
 *     2. '.' (dot) can occur more than once in a filename. Extension is
 *        the substring after the last dot.
 *
 * Returned Value:
 *   OK - The path refers to a valid long file name and has been properly
 *        stored in dirinfo.
 *   <0 - Otherwise an negated error is returned meaning that the string is
 *        not a valid long file name:
 *
 *        1. Contains characters not in the printable character set,
 *        2. Contains forbidden characters
 *        3. File name is too long.
 *
 ****************************************************************************/

#ifdef CONFIG_FAT_LFN
static inline int fat_parselfname(FAR const char **path,
                                  FAR struct fat_dirinfo_s *dirinfo,
                                  FAR char *terminator)
{
  FAR const char *node = *path;
  lfnchar ch;
  int ndx = 0;

  /* Loop until the name is successfully parsed or an error occurs */

  for (; ; )
    {
      /* Get the next character from the path */

#  ifdef CONFIG_FAT_LFN_UTF8
      if (fat_utf8toucs(&node, &ch) != OK)
        {
          goto errout;
        }
#  else
      ch = *node++;
#  endif

      /* Check if this the last byte in this node of the name */

      if ((ch == '\0' || ch == '/') && ndx != 0)
        {
          /* Null terminate the string */

          dirinfo->fd_lfname[ndx] = '\0';

          /* Return the remaining sub-string and the terminating character. */

          *terminator = (char)ch;
          *path       = node;
          return OK;
        }

      /* Accept only the printable character set (including space) */
#  ifdef CONFIG_FAT_LFN_UTF8
      /* We assume all ucs2 characters printable REVISIT? */

      else if (ch < ' ')
#  else
      else if (!isprint(ch))
#  endif
        {
          goto errout;
        }

      /* Reject printable characters forbidden by FAT */

      else if (ch == '"' || ch == '*' || ch == '/' || ch == ':'  ||
               ch == '<' || ch == '>' || ch == '?' || ch == '\\' ||
               ch == '|')
        {
          goto errout;
        }

      /* Check if the file name exceeds the size permitted. */

      if (ndx >= LDIR_MAXFNAME)
        {
          goto errout;
        }

      /* Save next character in the accumulated name */

      dirinfo->fd_lfname[ndx++] = ch;
    }

errout:
  dirinfo->fd_lfname[0] = '\0';
  return -EINVAL;
}
#endif

/****************************************************************************
 * Name: fat_createalias
 *
 * Description:  Given a valid long file name, create a short filename alias.
 *   Here are the rules for creation of the alias:
 *
 *   1. All uppercase
 *   2. All dots except the last deleted
 *   3. First 6 (uppercase) characters used as a base
 *   4. Then ~1.  The number is increased if the file already exists in the
 *      directory. If the number exceeds >10, then character stripped off the
 *       base.
 *   5. The extension is the first 3 uppercase chars of extension.
 *
 * This function is called only from fat_putlfname()
 *
 * Returned Value:
 *   OK - The alias was created correctly.
 *   <0 - Otherwise an negated error is returned.
 *
 ****************************************************************************/

#ifdef CONFIG_FAT_LFN
static inline int fat_createalias(FAR struct fat_dirinfo_s *dirinfo)
{
  uint8_t      ch;        /* Current character being processed */
  FAR lfnchar *ext;       /* Pointer to the extension substring */
  FAR lfnchar *src;       /* Pointer to the long file name source */
  int          len;       /* Total length of the long file name */
  int          namechars; /* Number of characters available in long name */
  int          extchars;  /* Number of characters available in long name extension */
  int          endndx;    /* Maximum index into the short name array */
  int          ndx;       /* Index to store next character */

  /* First, let's decide what is name and what is extension */

  for (len = 0, ext = NULL; dirinfo->fd_lfname[len] != '\0'; len++)
    {
      if (dirinfo->fd_lfname[len] == '.')
        {
          ext = &dirinfo->fd_lfname[len];
        }
    }

  if (ext)
    {
      ptrdiff_t tmp;

      /* ext points to the final '.'.  The difference in bytes from the
       * beginning of the string is then the name length.
       */

      tmp       = ext - (FAR lfnchar *)dirinfo->fd_lfname;
      namechars = tmp;

      /* And the rest, excluding the '.' is the extension. */

      extchars  = len - namechars - 1;
      ext++;
    }
  else
    {
      /* No '.' found.  It is all name and no extension. */

      namechars = len;
      extchars  = 0;
    }

#ifdef CONFIG_FAT_LCNAMES
  /* Alias are always all upper case */

  dirinfo->fd_ntflags = 0;
#endif

  /* Initialized the short name with all spaces */

  memset(dirinfo->fd_name, ' ', DIR_MAXFNAME);

  /* Handle a special case where there is no name.  Windows seems to use
   * the extension plus random stuff then ~1 to pad to 8 bytes.  Some
   * examples:
   *
   *   a.b          -> a.b          No long name
   *   a.,          -> A26BE~1._    Padded name to make unique, _ replaces ,
   *   .b           -> B1DD2~1      Extension used as name
   *   .bbbbbbb     -> BBBBBB~1     Extension used as name
   *   a.bbbbbbb    -> AAD39~1.BBB  Padded name to make unique.
   *   aaa.bbbbbbb  -> AAA~1.BBBB   Not padded, already unique?
   *   ,.bbbbbbb    -> _82AF~1.BBB  _ replaces ,
   *   +[],.bbbbbbb -> ____~1.BBB   _ replaces +[],
   */

  if (namechars < 1)
    {
      /* Use the extension as the name */

      DEBUGASSERT(ext && extchars > 0);
      src       = ext;
      ext       = NULL;
      namechars = extchars;
      extchars  = 0;
    }
  else
    {
       src       = (FAR lfnchar *)dirinfo->fd_lfname;
    }

  /* Then copy the name and extension, handling upper case conversions and
   * excluding forbidden characters.
   */

  ndx    = 0;  /* Position to write the next name character */
  endndx = 6;  /* Maximum index before we write ~! and switch to the extension */

  for (; ; )
    {
      /* Get the next byte from the path.  Break out of the loop if we
       * encounter the end of null-terminated the long file name string.
       */

#  ifdef CONFIG_FAT_LFN_UTF8
      /* Make sure ch is within printable characters */

      if (*src > 0x7f)
        {
          ch = (uint8_t)(*src++ & 0x1f) + 'A';
          if (ch >= '[')
            {
              ch -= ('[' - '0');
            }
        }
      else
        {
          ch = *src++;
        }
#  else
      ch = *src++;
#  endif

      if (ch == '\0')
        {
          /* This is the end of the source string. Do we need to add ~1.  We
           * will do that if we were parsing the name part when the end of
           * string was encountered.
           */

          if (endndx == 6)
            {
              /* Write the ~1 at the end of the name */

              dirinfo->fd_name[ndx++] = '~';
              dirinfo->fd_name[ndx]   = '1';
            }

          /* In any event, we are done */

          return OK;
        }

      /* Exclude those few characters included in long file names, but
       * excluded in short file name: '+', ',', ';', '=', '[', ']', and '.'
       */

      if (ch == '+' || ch == ',' || ch == '.' || ch == ';' ||
          ch == '=' || ch == '[' || ch == ']' || ch == '|' || ch == ' ')
        {
          /* Use the underbar character instead */

          ch = '_';
        }

      /* Handle lower case characters */

      ch = toupper(ch);

      /* We now have a valid character to add to the name or extension. */

      dirinfo->fd_name[ndx++] = ch;

      /* Did we just add a character to the name? */

      if (endndx == 6)
        {
          /* Decrement the number of characters available in the name
           * portion of the long name.
           */

          namechars--;

          /* Is it time to add ~1 to the string?  We will do that if
           * either (1) we have already added the maximum number of
           * characters to the short name, or (2) if there are no further
           * characters available in the name portion of the long name.
           */

          if (namechars < 1 || ndx == 6)
            {
              /* Write the ~1 at the end of the name */

              dirinfo->fd_name[ndx++] = '~';
              dirinfo->fd_name[ndx]   = '1';

              /* Then switch to the extension (if there is one) */

              if (!ext || extchars < 1)
                {
                  return OK;
                }

              ndx    = 8;
              endndx = 11;
              src    = ext;
            }
        }

      /* No.. we just added a character to the extension */

      else
        {
          /* Decrement the number of characters available in the name
           * portion of the long name
           */

          extchars--;

          /* Is the extension complete? */

          if (extchars < 1 || ndx == 11)
            {
              return OK;
            }
        }

#if defined(CONFIG_FAT_LFN_ALIAS_TRAILCHARS) && CONFIG_FAT_LFN_ALIAS_TRAILCHARS > 0
      /* Take first 6-N characters from beginning of filename and last N
       * characters from end of the filename. Useful for filenames like
       * "datafile123.txt".
       */

      if (ndx == 6 - CONFIG_FAT_LFN_ALIAS_TRAILCHARS
          && namechars > CONFIG_FAT_LFN_ALIAS_TRAILCHARS)
        {
          src += namechars - CONFIG_FAT_LFN_ALIAS_TRAILCHARS;
        }
#endif
    }
}
#endif

/****************************************************************************
 * Name: fat_findalias
 *
 * Description:  Make sure that the short alias for the long file name is
 *   unique, ie., that there is no other
 *
 * NOTE: This function does not restore the directory entry that was in the
 * sector cache
 *
 * Returned Value:
 *   OK - The alias is unique.
 *   <0 - Otherwise an negated error is returned.
 *
 ****************************************************************************/

#ifdef CONFIG_FAT_LFN
static inline int fat_findalias(FAR struct fat_mountpt_s *fs,
                                FAR struct fat_dirinfo_s *dirinfo)
{
  struct fat_dirinfo_s tmpinfo;

  /* Save the current directory info. */

  memcpy(&tmpinfo, dirinfo, sizeof(struct fat_dirinfo_s));

  /* Then re-initialize to the beginning of the current directory, starting
   * with the first entry.
   */

  tmpinfo.dir.fd_startcluster = tmpinfo.dir.fd_currcluster;
  tmpinfo.dir.fd_currsector   = tmpinfo.fd_seq.ds_startsector;
  tmpinfo.dir.fd_index        = 0;

  /* Search for the single short file name directory entry in this
   * directory.
   */

  return fat_findsfnentry(fs, &tmpinfo);
}
#endif

/****************************************************************************
 * Name: fat_uniquealias
 *
 * Description:  Make sure that the short alias for the long file name is
 *   unique, modifying the alias as necessary to assure uniqueness.
 *
 * NOTE: This function does not restore the directory entry that was in the
 * sector cache
 *
 *   information upon return.
 * Returned Value:
 *   OK - The alias is unique.
 *   <0 - Otherwise an negated error is returned.
 *
 ****************************************************************************/

#ifdef CONFIG_FAT_LFN
static inline int fat_uniquealias(FAR struct fat_mountpt_s *fs,
                                  FAR struct fat_dirinfo_s *dirinfo)
{
  int tilde;
  int lsdigit;
  int ret;
  int i;

  /* Find the position of the tilde character in the short name.  The tilde
   * can not occur in positions 0 or 7:
   */

  for (tilde = 1; tilde < 7 && dirinfo->fd_name[tilde] != '~'; tilde++)
    {
      /* Empty */
    }

  if (tilde >= 7)
    {
      return -EINVAL;
    }

  /* The least significant number follows the digit (and must be '1') */

  lsdigit = tilde + 1;
  DEBUGASSERT(dirinfo->fd_name[lsdigit] == '1');

#ifdef CONFIG_FAT_LFN_ALIAS_HASH
  /* Add a hash of the long filename to the short filename, to reduce
   * collisions.
   */

  if ((ret = fat_findalias(fs, dirinfo)) == OK)
    {
      uint16_t hash = dirinfo->fd_seq.ds_offset;

      for (i = 0; dirinfo->fd_lfname[i] != '\0'; i++)
        {
          hash = ((hash << 5) + hash) ^ dirinfo->fd_lfname[i];
        }

      for (i = 0; i < tilde - 2; i++)
        {
          uint8_t nibble = (hash >> (i * 4)) & 0x0f;
          FAR const char *digits = "0123456789ABCDEF";

          dirinfo->fd_name[tilde - 1 - i] = digits[nibble];
        }
    }
  else if (ret == -ENOENT)
    {
      return OK; /* Alias was unique already */
    }
  else
    {
      return ret; /* Other error */
    }
#endif

  /* Search for the single short file name directory entry in this
   * directory.
   */

  while ((ret = fat_findalias(fs, dirinfo)) == OK)
    {
      /* Adjust the numeric value after the '~' to make the file name
       * unique.
       */

      for (i = lsdigit; i > 0; i--)
        {
          /* If we have backed up to the tilde position, then we have to move
           * the tilde back one position.
           */

          if (i == tilde)
            {
              /* Is there space to back up the tilde? */

              if (tilde <= 1)
                {
                  /* No.. then we cannot add the name to the directory.
                   * What is the likelihood of that happening?
                   */

                  return -ENOSPC;
                }

              /* Back up the tilde and break out of the inner loop */

              tilde--;
              dirinfo->fd_name[tilde]     = '~';
              dirinfo->fd_name[tilde + 1] = '1';
              break;
            }

          /* We are not yet at the tilde,.  Check if this digit has already
           * reached its maximum value.
           */

          else if (dirinfo->fd_name[i] < '9')
            {
              /* No, it has not.. just increment the LS digit and break out
               * of the inner loop.
               */

              dirinfo->fd_name[i]++;
              break;
            }

          /* Yes.. Reset the digit to '0' and loop to adjust the digit before
           * this one.
           */

          else
            {
              dirinfo->fd_name[i] = '0';
            }
        }
    }

  /* The while loop terminated because of an error; fat_findalias()
   * returned something other than OK.  The only acceptable error is
   * -ENOENT, meaning that the short file name directory does not
   * exist in this directory.
   */

  if (ret == -ENOENT)
    {
      ret = OK;
    }

  return ret;
}
#endif

/****************************************************************************
 * Name: fat_path2dirname
 *
 * Description:  Convert a user filename into a properly formatted FAT
 *   (short 8.3) filename as it would appear in a directory entry.
 *
 ****************************************************************************/

static int fat_path2dirname(FAR const char **path,
                            FAR struct fat_dirinfo_s *dirinfo,
                            FAR char *terminator)
{
#ifdef CONFIG_FAT_LFN
  int ret;

  /* Assume no long file name */

  dirinfo->fd_lfname[0] = '\0';

  /* Then parse the (assumed) 8+3 short file name */

  ret = fat_parsesfname(path, dirinfo, terminator);
  if (ret < 0)
    {
      /* No, the name is not a valid short 8+3 file name. Try parsing
       * the long file name.
       */

      ret = fat_parselfname(path, dirinfo, terminator);
    }

  return ret;
#else
  /* Only short, 8+3 filenames supported */

  return fat_parsesfname(path, dirinfo, terminator);
#endif
}

/****************************************************************************
 * Name: fat_findsfnentry
 *
 * Description: Find a short file name directory entry.  Returns OK if the
 *  directory exists; -ENOENT if it does not.
 *
 ****************************************************************************/

static int fat_findsfnentry(FAR struct fat_mountpt_s *fs,
                            FAR struct fat_dirinfo_s *dirinfo)
{
  uint16_t diroffset;
  FAR uint8_t *direntry;
#ifdef CONFIG_FAT_LFN
  off_t startsector;
#endif
  int ret;

  /* Save the starting sector of the directory.  This is not really needed
   * for short name entries, but this keeps things consistent with long
   * file name entries..
   */

#ifdef CONFIG_FAT_LFN
  startsector = dirinfo->dir.fd_currsector;
#endif

  /* Search, beginning with the current sector, for a directory entry with
   * the matching short name
   */

  for (; ; )
    {
      /* Read the next sector into memory */

      ret = fat_fscacheread(fs, dirinfo->dir.fd_currsector);
      if (ret < 0)
        {
          return ret;
        }

      /* Get a pointer to the directory entry */

      diroffset = DIRSEC_BYTENDX(fs, dirinfo->dir.fd_index);
      direntry  = &fs->fs_buffer[diroffset];

      /* Check if we are at the end of the directory */

      if (direntry[DIR_NAME] == DIR0_ALLEMPTY)
        {
          return -ENOENT;
        }

      /* Check if we have found the directory entry that we are looking for */

      if (direntry[DIR_NAME] != DIR0_EMPTY &&
          !(DIR_GETATTRIBUTES(direntry) & FATATTR_VOLUMEID) &&
          !memcmp(&direntry[DIR_NAME], dirinfo->fd_name, DIR_MAXFNAME))
        {
          /* Yes.. Return success */

          dirinfo->fd_seq.ds_sector      = fs->fs_currentsector;
          dirinfo->fd_seq.ds_offset      = diroffset;
#ifdef CONFIG_FAT_LFN
          dirinfo->fd_seq.ds_cluster     = dirinfo->dir.fd_currcluster;
          dirinfo->fd_seq.ds_startsector = startsector;

          /* Position the last long file name directory entry at the same
           * position.
           */

          dirinfo->fd_seq.ds_lfnsector   = dirinfo->fd_seq.ds_sector;
          dirinfo->fd_seq.ds_lfnoffset   = dirinfo->fd_seq.ds_offset;
          dirinfo->fd_seq.ds_lfncluster  = dirinfo->fd_seq.ds_cluster;
#endif
          return OK;
        }

      /* No... get the next directory index and try again */

      if (fat_nextdirentry(fs, &dirinfo->dir) != OK)
        {
          return -ENOENT;
        }
    }
}

/****************************************************************************
 * Name: fat_cmplfnchunk
 *
 * Description:  There are 13 characters per LFN entry, broken up into three
 *   chunks for characters 1-5, 6-11, and 12-13.  This function will perform
 *   the comparison of a single chunk.
 *
 ****************************************************************************/

#ifdef CONFIG_FAT_LFN
static bool fat_cmplfnchunk(FAR uint8_t *chunk, FAR const lfnchar *substr,
                            int nchunk)
{
  uint16_t wch;
  lfnchar ch;
  int     i;

  /* Check bytes 1-nchunk */

  for (i = 0; i < nchunk; i++)
    {
      /* Get the next character from the name string (which might be the NUL
       * terminating character).
       */

      if (*substr == '\0')
        {
          ch = '\0';
        }
      else
        {
          ch = *substr++;
        }

      /* Get the next unicode character from the chunk.  We only handle
       * ASCII. For ASCII, the upper byte should be zero and the lower
       * should match the ASCII code.
       */

      wch = fat_getuint16((FAR uint8_t *)chunk);
#  ifdef CONFIG_FAT_LFN_UTF8
      if (wch != ch)
#  else
      if ((wch & 0xff) != (uint16_t)ch)
#  endif
        {
          return false;
        }

      /* The characters match.  If we just matched the NUL terminating
       * character, then the strings match and we are finished.
       */

      if (ch == '\0')
        {
          return true;
        }

      /* Try the next character from the directory entry. */

      chunk += sizeof(uint16_t);
    }

  /* All of the characters in the chunk match.. Return success */

  return true;
}
#endif

/****************************************************************************
 * Name: fat_cmplfname
 *
 * Description: Given an LFN directory entry, compare a substring of the name
 *   to a portion in the directory entry.
 *
 ****************************************************************************/

#ifdef CONFIG_FAT_LFN
static bool fat_cmplfname(FAR const uint8_t *direntry,
                          FAR const lfnchar *substr)
{
  FAR uint8_t *chunk;
  int len;
  bool match;

  /* How much of string do we have to compare? (including the NUL
   * terminator).
   */

  for (len = 1; substr[len - 1] != '\0'; len++)
    {
      /* Empty */
    }

  /* Check bytes 1-5 */

  chunk = LDIR_PTRWCHAR1_5(direntry);
  match = fat_cmplfnchunk(chunk, substr, 5);
  if (match && len > 5)
    {
      /* Check bytes 6-11 */

      chunk = LDIR_PTRWCHAR6_11(direntry);
      match = fat_cmplfnchunk(chunk, &substr[5], 6);
      if (match && len > 11)
        {
          /* Check bytes 12-13 */

          chunk = LDIR_PTRWCHAR12_13(direntry);
          match = fat_cmplfnchunk(chunk, &substr[11], 2);
        }
    }

  return match;
}
#endif

/****************************************************************************
 * Name: fat_findlfnentry
 *
 * Description: Find a sequence of long file name directory entries.
 *
 * NOTE: As a side effect, this function returns with the sector containing
 *   the short file name directory entry in the cache.
 *
 ****************************************************************************/

#ifdef CONFIG_FAT_LFN
static inline int fat_findlfnentry(FAR struct fat_mountpt_s *fs,
                                   FAR struct fat_dirinfo_s *dirinfo)
{
  FAR uint8_t *direntry;
  uint16_t diroffset;
  uint8_t  lastseq;
  uint8_t  seqno;
  uint8_t  nfullentries;
  uint8_t  nentries;
  uint8_t  remainder;
  uint8_t  checksum = 0;
  off_t    startsector;
  int      offset;
  int      namelen;
  int      ret;

  /* Get the length of the long file name (size of the fd_lfname array is
   * LDIR_MAXFNAME+1 we do not have to check the length of the string).
   */

  for (namelen = 0; dirinfo->fd_lfname[namelen] != '\0'; namelen++)
    {
      /* Empty */
    }

  DEBUGASSERT(namelen <= LDIR_MAXFNAME + 1);

  /* How many LFN directory entries are we expecting? */

  nfullentries = namelen / LDIR_MAXLFNCHARS;
  remainder    = namelen - nfullentries * LDIR_MAXLFNCHARS;
  nentries     = nfullentries;
  if (remainder > 0)
    {
      nentries++;
    }

  DEBUGASSERT(nentries > 0 && nentries <= LDIR_MAXLFNS);

  /* This is the first sequence number we are looking for, the sequence
   * number of the last LFN entry (remember that they appear in reverse
   * order.. from last to first).
   */

  lastseq = LDIR0_LAST | nentries;
  seqno   = lastseq;

  /* Save the starting sector of the directory.  This is needed later to
   * re-scan the directory, looking duplicate short alias names.
   */

  startsector   = dirinfo->dir.fd_currsector;

  /* Search, beginning with the current sector, for a directory entry this
   * the match shore name
   */

  for (; ; )
    {
      /* Read the next sector into memory */

      ret = fat_fscacheread(fs, dirinfo->dir.fd_currsector);
      if (ret < 0)
        {
          return ret;
        }

      /* Get a pointer to the directory entry */

      diroffset = DIRSEC_BYTENDX(fs, dirinfo->dir.fd_index);
      direntry  = &fs->fs_buffer[diroffset];

      /* Check if we are at the end of the directory */

      if (direntry[DIR_NAME] == DIR0_ALLEMPTY)
        {
          return -ENOENT;
        }

      /* Is this an LFN entry?  Does it have the sequence number we are
       * looking for?
       */

      if (LDIR_GETATTRIBUTES(direntry) != LDDIR_LFNATTR ||
          LDIR_GETSEQ(direntry) != seqno)
        {
          /* No, restart the search at the next entry */

          seqno = lastseq;
          goto next_entry;
        }

      /* Yes.. If this is not the "last" LFN entry, then the checksum must
       * also be the same.
       */

      if (seqno == lastseq)
        {
          /* Just save the checksum for subsequent checks */

          checksum = LDIR_GETCHECKSUM(direntry);
        }

      /* Not the first entry in the sequence.  Does the checksum match the
       * previous sequences?
       */

      else if (checksum != LDIR_GETCHECKSUM(direntry))
        {
          /* No, restart the search at the next entry */

          seqno = lastseq;
          goto next_entry;
        }

      /* Check if the name substring in this LFN matches the corresponding
       * substring of the name we are looking for.
       */

      offset = ((seqno & LDIR0_SEQ_MASK) - 1) * LDIR_MAXLFNCHARS;
      if (fat_cmplfname(direntry, &dirinfo->fd_lfname[offset]))
        {
          /* Yes.. it matches.  Check the sequence number.  Is this the
           * "last" LFN entry (i.e., the one that appears first)?
           */

          if (seqno == lastseq)
            {
              /* Yes.. Save information about this LFN entry position */

              dirinfo->fd_seq.ds_lfnsector   = fs->fs_currentsector;
              dirinfo->fd_seq.ds_lfnoffset   = diroffset;
              dirinfo->fd_seq.ds_lfncluster  = dirinfo->dir.fd_currcluster;
              dirinfo->fd_seq.ds_startsector = startsector;
              seqno &= LDIR0_SEQ_MASK;
            }

          /* Is this the first sequence number (i.e., the LFN entry that
           * will appear last)?
           */

          if (seqno == 1)
            {
              /* We have found all of the LFN entries.  The next directory
               * entry should be the one containing the short file name
               * alias and all of the meat about the file or directory.
               */

              if (fat_nextdirentry(fs, &dirinfo->dir) != OK)
                {
                  return -ENOENT;
                }

              /* Make sure that the directory entry is in the sector cache */

              ret = fat_fscacheread(fs, dirinfo->dir.fd_currsector);
              if (ret < 0)
                {
                  return ret;
                }

              /* Get a pointer to the directory entry */

              diroffset = DIRSEC_BYTENDX(fs, dirinfo->dir.fd_index);
              direntry  = &fs->fs_buffer[diroffset];

              /* Verify the checksum */

              if (fat_lfnchecksum(&direntry[DIR_NAME]) == checksum)
                {
                  /* Success! Save the position of the directory entry and
                   * return success.
                   */

                  dirinfo->fd_seq.ds_sector  = fs->fs_currentsector;
                  dirinfo->fd_seq.ds_offset  = diroffset;
                  dirinfo->fd_seq.ds_cluster = dirinfo->dir.fd_currcluster;
                  return OK;
                }

              /* Bad news.. reset and continue with this entry (which is
               * probably not an LFN entry unless the file system is
               * seriously corrupted.
               */

              seqno = lastseq;
              continue;
            }

          /* No.. there are more LFN entries to go.  Decrement the sequence
           * number and check the next directory entry.
           */

          seqno--;
        }
      else
        {
          /* No.. the names do not match.  Restart the search at the next
           * entry.
           */

          seqno = lastseq;
        }

      /* Continue at the next directory entry */

next_entry:
      if (fat_nextdirentry(fs, &dirinfo->dir) != OK)
        {
          return -ENOENT;
        }
    }
}
#endif

/****************************************************************************
 * Name: fat_allocatesfnentry
 *
 * Description: Find a free directory entry for a short file name entry.
 *
 ****************************************************************************/

static inline int fat_allocatesfnentry(FAR struct fat_mountpt_s *fs,
                                       FAR struct fat_dirinfo_s *dirinfo)
{
  FAR uint8_t *direntry;
  uint16_t diroffset;
#ifdef CONFIG_FAT_LFN
  off_t    startsector;
#endif
  uint8_t  ch;
  int      ret;

  /* Save the sector number of the first sector of the directory.  We don't
   * really need this for short file name entries; this is just done for
   * consistency with the long file name logic.
   */

#ifdef CONFIG_FAT_LFN
  startsector = dirinfo->dir.fd_currsector;
#endif

  /* Then search for a free short file name directory entry */

  for (; ; )
    {
      /* Read the directory sector into fs_buffer */

      ret = fat_fscacheread(fs, dirinfo->dir.fd_currsector);
      if (ret < 0)
        {
          /* Make sure that the return value is NOT -ENOSPC */

          return -EIO;
        }

      /* Get a pointer to the entry at fd_index */

      diroffset = (dirinfo->dir.fd_index & DIRSEC_NDXMASK(fs)) * DIR_SIZE;
      direntry  = &fs->fs_buffer[diroffset];

      /* Check if this directory entry is empty */

      ch = direntry[DIR_NAME];
      if (ch == DIR0_ALLEMPTY || ch == DIR0_EMPTY)
        {
          /* It is empty -- we have found a directory entry */

          dirinfo->fd_seq.ds_sector       = fs->fs_currentsector;
          dirinfo->fd_seq.ds_offset       = diroffset;
#ifdef CONFIG_FAT_LFN
          dirinfo->fd_seq.ds_cluster      = dirinfo->dir.fd_currcluster;
          dirinfo->fd_seq.ds_startsector  = startsector;

          /* Set the "last" long file name offset to the same entry */

          dirinfo->fd_seq.ds_lfnsector    = dirinfo->fd_seq.ds_sector;
          dirinfo->fd_seq.ds_lfnoffset    = dirinfo->fd_seq.ds_offset;
          dirinfo->fd_seq.ds_lfncluster   = dirinfo->fd_seq.ds_cluster;
#endif
          return OK;
        }

      /* It is not empty try the next one */

      ret = fat_nextdirentry(fs, &dirinfo->dir);
      if (ret < 0)
        {
          /* This will return -ENOSPC if we have examined all of the
           * directory entries without finding a free entry.
           */

          return ret;
        }
    }
}

/****************************************************************************
 * Name: fat_allocatelfnentry
 *
 * Description: Find a sequence of free directory entries for a several long
 *   and one short file name entry.
 *
 * On entry, dirinfo.dir refers to the first interesting entry the directory.
 *
 ****************************************************************************/

#ifdef CONFIG_FAT_LFN
static inline int fat_allocatelfnentry(FAR struct fat_mountpt_s *fs,
                                       FAR struct fat_dirinfo_s *dirinfo)
{
  FAR uint8_t *direntry;
  uint16_t diroffset;
  off_t    startsector;
  uint8_t  nentries;
  uint8_t  remainder;
  uint8_t  needed;
  uint8_t  ch;
  int      namelen;
  int      ret;

  /* Get the length of the long file name (size of the fd_lfname array is
   * LDIR_MAXFNAME+1 we do not have to check the length of the string).
   */

  for (namelen = 0; dirinfo->fd_lfname[namelen] != '\0'; namelen++)
    {
      /* Empty */
    }

  DEBUGASSERT(namelen <= LDIR_MAXFNAME + 1);

  /* How many LFN directory entries are we expecting? */

  nentries   = namelen / LDIR_MAXLFNCHARS;
  remainder  = namelen - nentries * LDIR_MAXLFNCHARS;
  if (remainder > 0)
    {
      nentries++;
    }

  DEBUGASSERT(nentries > 0 && nentries <= LDIR_MAXLFNS);

  /* Plus another for short file name entry that follows the sequence of LFN
   * entries.
   */

  nentries++;

  /* Save the sector number of the first sector of the directory.  We will
   * need this later for re-scanning the directory to verify that a FAT file
   * name is unique.
   */

  startsector = dirinfo->dir.fd_currsector;

  /* Now, search the directory looking for a sequence for free entries that
   * long.
   */

  needed = nentries;
  for (; ; )
    {
      /* Read the directory sector into fs_buffer */

      ret = fat_fscacheread(fs, dirinfo->dir.fd_currsector);
      if (ret < 0)
        {
          /* Make sure that the return value is NOT -ENOSPC */

          return -EIO;
        }

      /* Get a pointer to the entry at fd_index */

      diroffset = (dirinfo->dir.fd_index & DIRSEC_NDXMASK(fs)) * DIR_SIZE;
      direntry  = &fs->fs_buffer[diroffset];

      /* Check if this directory entry is empty */

      ch = LDIR_GETSEQ(direntry);
      if (ch == DIR0_ALLEMPTY || ch == DIR0_EMPTY)
        {
          /* It is empty -- we have found a directory entry.  Is this the
           * "last" LFN entry (i.e., the one that occurs first)?
           */

          if (needed == nentries)
            {
              /* Yes.. remember the position of this entry */

              dirinfo->fd_seq.ds_lfnsector    = fs->fs_currentsector;
              dirinfo->fd_seq.ds_lfnoffset    = diroffset;
              dirinfo->fd_seq.ds_lfncluster   = dirinfo->dir.fd_currcluster;
              dirinfo->fd_seq.ds_startsector  = startsector;
            }

          /* Is this last entry we need (i.e., the entry for the short
           * file name entry)?
           */

          if (needed <= 1)
            {
              /* Yes.. remember the position of this entry and return
               * success.
               */

              dirinfo->fd_seq.ds_sector  = fs->fs_currentsector;
              dirinfo->fd_seq.ds_offset  = diroffset;
              dirinfo->fd_seq.ds_cluster = dirinfo->dir.fd_currcluster;
              return OK;
            }

          /* Otherwise, just decrement the number of directory entries
           * needed and continue looking.
           */

          needed--;
        }

      /* The directory entry is not available */

      else
        {
          /* Reset the search and continue looking */

          needed = nentries;
        }

      /* Try the next directory entry */

      ret = fat_nextdirentry(fs, &dirinfo->dir);
      if (ret < 0)
        {
          /* This will return -ENOSPC if we have examined all of the
           * directory entries without finding a free entry.
           */

          return ret;
        }
    }
}
#endif

/****************************************************************************
 * Name: fat_getsfname
 *
 * Description:  Get the 8.3 filename from a directory entry.  On entry, the
 *  short file name entry is already in the cache.
 *
 ****************************************************************************/

static inline int fat_getsfname(FAR uint8_t *direntry, FAR char *buffer,
                                unsigned int buflen)
{
#ifdef CONFIG_FAT_LCNAMES
  uint8_t ntflags;
#endif
  int  ch;
  int  ndx;

  /* Check if we will be doing upper to lower case conversions */

#ifdef CONFIG_FAT_LCNAMES
  ntflags = DIR_GETNTRES(direntry);
#endif

  /* Reserve a byte for the NUL terminator */

  buflen--;

  /* Get the 8-byte filename */

  for (ndx = 0; ndx < 8 && buflen > 0; ndx++)
    {
      /* Get the next filename character from the directory entry */

      ch = direntry[ndx];

      /* Any space (or ndx==8) terminates the filename */

      if (ch == ' ')
        {
          break;
        }

      /* In this version, we never write 0xe5 in the directory filenames
       * (because we do not handle any character sets where 0xe5 is valid
       * in a filaname), but we could eencounter this in a filesystem
       * written by some other system
       */

      if (ndx == 0 && ch == DIR0_E5)
        {
          ch = 0xe5;
        }

      /* Check if we should perform upper to lower case conversion
       * of the (whole) filename.
       */

#ifdef CONFIG_FAT_LCNAMES
      if (ntflags & FATNTRES_LCNAME && isupper(ch))
        {
          ch = tolower(ch);
        }
#endif

      /* Copy the next character into the filename */

      *buffer++ = ch;
      buflen--;
    }

  /* Check if there is an extension */

  if (direntry[8] != ' ' && buflen > 0)
    {
      /* Yes, output the dot before the extension */

      *buffer++ = '.';
      buflen--;

      /* Then output the (up to) 3 character extension */

      for (ndx = 8; ndx < 11 && buflen > 0; ndx++)
        {
          /* Get the next extensions character from the directory entry */

          ch = direntry[DIR_NAME + ndx];

          /* Any space (or ndx==11) terminates the extension */

          if (ch == ' ')
            {
              break;
            }

          /* Check if we should perform upper to lower case conversion
           * of the (whole) filename.
           */

#ifdef CONFIG_FAT_LCNAMES
          if (ntflags & FATNTRES_LCEXT && isupper(ch))
            {
              ch = tolower(ch);
            }
#endif

          /* Copy the next character into the filename */

          *buffer++ = ch;
          buflen--;
        }
    }

  /* Put a null terminator at the end of the filename.  We don't have to
   * check if there is room because we reserved a byte for the NUL
   * terminator at the beginning of this function.
   */

  *buffer = '\0';
  return OK;
}

/****************************************************************************
 * Name: fat_getlfnchunk
 *
 * Description:  There are 13 characters per LFN entry, broken up into three
 *   chunks for characters 1-5, 6-11, and 12-13.  This function will get the
 *   file name characters from one chunk.
 *
 ****************************************************************************/

#ifdef CONFIG_FAT_LFN
static void fat_getlfnchunk(FAR uint8_t *chunk, FAR lfnchar *dest,
                            int nchunk)
{
  uint16_t wch;
  int i;

  /* Copy bytes 1-nchunk */

  for (i = 0; i < nchunk; i++)
    {
      /* Get the next unicode character from the chunk.  We only handle
       * ASCII.  For ASCII, the upper byte should be zero and the lower
       * should match the ASCII code.
       */

      wch = fat_getuint16(chunk);
#  ifdef CONFIG_FAT_LFN_UTF8
      *dest++ = wch;
#  else
      *dest++ = (uint8_t)(wch & 0xff);
#  endif
      chunk += sizeof(uint16_t);
    }
}
#endif

/****************************************************************************
 * Name: fat_getlfname
 *
 * Description:  Get the long filename from a sequence of directory entries.
 *   On entry, the "last" long file name entry is in the cache.  Returns with
 *   the short file name entry in the cache.
 *
 ****************************************************************************/

#ifdef CONFIG_FAT_LFN
static inline int fat_getlfname(FAR struct fat_mountpt_s *fs,
                                FAR struct fs_dirent_s *dir,
                                FAR struct dirent *entry)
{
  FAR struct fat_dirent_s *fdir;
  FAR uint8_t *direntry;
  lfnchar  lfname[LDIR_MAXLFNCHARS];
  uint16_t diroffset;
  uint8_t  seqno;
  uint8_t  rawseq;
  uint8_t  offset;
  uint8_t  checksum;
  int      nsrc;
  int      ret;
  int      i;

  /* Get a reference to the current directory entry */

  fdir = (FAR struct fat_dirent_s *)dir;
  diroffset = (fdir->dir.fd_index & DIRSEC_NDXMASK(fs)) * DIR_SIZE;
  direntry  = &fs->fs_buffer[diroffset];

  /* Get the starting sequence number */

  seqno = LDIR_GETSEQ(direntry);
  DEBUGASSERT((seqno & LDIR0_LAST) != 0);

  /* Sanity check */

  rawseq = (seqno & LDIR0_SEQ_MASK);
  if (rawseq < 1 || rawseq > LDIR_MAXLFNS)
    {
      return -EINVAL;
    }

  /* Save the checksum value */

  checksum = LDIR_GETCHECKSUM(direntry);

  /* Loop until the whole file name has been transferred */

  for (; ; )
    {
#  ifdef CONFIG_FAT_LFN_UTF8
      /* Get the string offset associated with the "last" entry. */

      /* Extract and convert the unicode name */

      fat_getlfnchunk(LDIR_PTRWCHAR1_5(direntry), lfname, 5);
      fat_getlfnchunk(LDIR_PTRWCHAR6_11(direntry), &lfname[5], 6);
      fat_getlfnchunk(LDIR_PTRWCHAR12_13(direntry), &lfname[11], 2);

      /* Ignore trailing spaces on the "last" directory entry.  The
       * number of characters available is LDIR_MAXLFNCHARS or that
       * minus the number of trailing spaces on the "last" directory
       * entry.
       */

      nsrc = LDIR_MAXLFNCHARS;
      if ((seqno & LDIR0_LAST) != 0)
        {
          /* Reduce the number of characters by the number of trailing
           * spaces, init chars (0xffff) and '\0'.
           */

          for (; nsrc > 0 && (lfname[nsrc - 1] == ' '  ||
                              lfname[nsrc - 1] == '\0' ||
                              lfname[nsrc - 1] == 0xffff); nsrc--);

          /* Add a null terminator to the destination string (the actual
           * length of the destination buffer is NAME_MAX+1, so the NUL
           * terminator will fit).
           */

          entry->d_name[NAME_MAX] = '\0';
          offset = NAME_MAX;
        }

      /* Then transfer the characters */

      for (i = nsrc - 1; i >= 0; i--)
        {
          offset = fat_ucstoutf8((FAR uint8_t *)entry->d_name,
                      offset, lfname[i]);
        }
#  else
      /* Get the string offset associated with the "last" entry. */

      offset = (rawseq - 1) * LDIR_MAXLFNCHARS;

      /* Will any of this file name fit into the destination buffer? */

      if (offset < NAME_MAX)
        {
          /* Yes.. extract and convert the unicode name */

          fat_getlfnchunk(LDIR_PTRWCHAR1_5(direntry), lfname, 5);
          fat_getlfnchunk(LDIR_PTRWCHAR6_11(direntry), &lfname[5], 6);
          fat_getlfnchunk(LDIR_PTRWCHAR12_13(direntry), &lfname[11], 2);

          /* Ignore trailing spaces on the "last" directory entry.  The
           * number of characters available is LDIR_MAXLFNCHARS or that
           * minus the number of trailing spaces on the "last" directory
           * entry.
           */

          nsrc = LDIR_MAXLFNCHARS;
          if ((seqno & LDIR0_LAST) != 0)
            {
              /* Reduce the number of characters by the number of trailing
               * spaces, init chars (0xff) and '\0'.
               */

              for (; nsrc > 0 && (lfname[nsrc - 1] == ' '  ||
                                  lfname[nsrc - 1] == '\0' ||
                                  lfname[nsrc - 1] == 0xff); nsrc--);

              /* Further reduce the length so that it fits in the destination
               * buffer.
               */

              if (offset + nsrc > NAME_MAX)
                {
                  nsrc = NAME_MAX - offset;
                }

              /* Add a null terminator to the destination string (the actual
               * length of the destination buffer is NAME_MAX+1, so the NUL
               * terminator will fit).
               */

              entry->d_name[offset + nsrc] = '\0';
            }

          /* Then transfer the characters */

          for (i = 0; i < nsrc && offset + i < NAME_MAX; i++)
            {
              entry->d_name[offset + i] = lfname[i];
            }
        }
#endif

      /* Read next directory entry */

      if (fat_nextdirentry(fs, &fdir->dir) != OK)
        {
          return -ENOENT;
        }

      /* Make sure that the directory sector into the sector cache */

      ret = fat_fscacheread(fs, fdir->dir.fd_currsector);
      if (ret < 0)
        {
          return ret;
        }

      /* Get a reference to the current directory entry */

      diroffset = (fdir->dir.fd_index & DIRSEC_NDXMASK(fs)) * DIR_SIZE;
      direntry  = &fs->fs_buffer[diroffset];

      /* Get the next expected sequence number. */

      seqno = --rawseq;
      if (seqno < 1)
        {
#  ifdef CONFIG_FAT_LFN_UTF8
          /* We must left align the d_name after utf8 processing */

          if (offset > 0)
            {
              memmove(entry->d_name, &entry->d_name[offset],
                  (NAME_MAX + 1) - offset);
            }
#  endif

          /* We just completed processing the "first" long file name entry
           * and we just read the short file name entry.  Verify that the
           * checksum of the short file name matches the checksum that we
           * found in the long file name entries.
           */

          if (fat_lfnchecksum(direntry) == checksum)
            {
              /* Yes.. return success! */

              return OK;
            }

          /* No, the checksum is bad. */

          return -EINVAL;
        }

      /* Verify the next long file name entry. Is this an LFN entry?  Does it
       * have the sequence number we are looking for?  Does the checksum
       * match the previous entries?
       */

      if (LDIR_GETATTRIBUTES(direntry) != LDDIR_LFNATTR ||
          LDIR_GETSEQ(direntry)        != seqno ||
          LDIR_GETCHECKSUM(direntry)   != checksum)
        {
          return -EINVAL;
        }
    }
}
#endif

/****************************************************************************
 * Name: fat_putsfname
 *
 * Description: Write the short directory entry name.
 *
 * Assumption:  The directory sector is in the cache.
 *
 ****************************************************************************/

static int fat_putsfname(FAR struct fat_mountpt_s *fs,
                         FAR struct fat_dirinfo_s *dirinfo)
{
  FAR uint8_t *direntry = &fs->fs_buffer[dirinfo->fd_seq.ds_offset];

  /* Write the short directory entry */

  memcpy(&direntry[DIR_NAME], dirinfo->fd_name, DIR_MAXFNAME);
#ifdef CONFIG_FAT_LCNAMES
  DIR_PUTNTRES(direntry, dirinfo->fd_ntflags);
#else
  DIR_PUTNTRES(direntry, 0);
#endif
  fs->fs_dirty = true;
  return OK;
}

/****************************************************************************
 * Name: fat_initlfname
 *
 * Description:  There are 13 characters per LFN entry, broken up into three
 *   chunks for characters 1-5, 6-11, and 12-13.  This function will put the
 *   0xffff characters into one chunk.
 *
 ****************************************************************************/

#ifdef CONFIG_FAT_LFN
static void fat_initlfname(FAR uint8_t *chunk, int nchunk)
{
  int i;

  /* Initialize unicode characters 1-nchunk */

  for (i = 0; i < nchunk; i++)
    {
      /* The write the 16-bit 0xffff character into the directory entry. */

      fat_putuint16((FAR uint8_t *)chunk, (uint16_t)0xffff);
      chunk += sizeof(uint16_t);
    }
}
#endif

/****************************************************************************
 * Name: fat_putlfnchunk
 *
 * Description:  There are 13 characters per LFN entry, broken up into three
 *   chunks for characters 1-5, 6-11, and 12-13.  This function will put the
 *   file name characters into one chunk.
 *
 ****************************************************************************/

#ifdef CONFIG_FAT_LFN
static void fat_putlfnchunk(FAR uint8_t *chunk, FAR const lfnchar *src,
                            int nchunk)
{
  uint16_t wch;
  int i;

  /* Write bytes 1-nchunk */

  for (i = 0; i < nchunk; i++)
    {
      /* Get the next ascii character from the name substring and convert it
       * to unicode.  The upper byte should be zero and the lower should be
       * the ASCII code.  The write the unicode character into the directory
       * entry.
       */

      wch = (uint16_t)*src++;
      fat_putuint16(chunk, wch);
      chunk += sizeof(uint16_t);
    }
}
#endif

/****************************************************************************
 * Name: fat_putlfname
 *
 * Description:
 *   Write the long filename into a sequence of directory entries. On entry,
 *   the "last" long file name entry is in the cache.  Returns with the
 *   short file name entry in the cache.
 *
 ****************************************************************************/

#ifdef CONFIG_FAT_LFN
static int fat_putlfname(FAR struct fat_mountpt_s *fs,
                         FAR struct fat_dirinfo_s *dirinfo)
{
  FAR uint8_t *direntry;
  uint16_t diroffset;
  uint8_t  nfullentries;
  uint8_t  nentries;
  uint8_t  remainder;
  uint8_t  offset;
  uint8_t  seqno;
  uint8_t  checksum;
  off_t    startsector;
  int      namelen;
  int      ret;

  /* Get the length of the long file name (size of the fd_lfname array is
   * LDIR_MAXFNAME+1 we do not have to check the length of the string).
   * NOTE that remainder is conditionally incremented to include the NUL
   * terminating character that may also need be written to the directory
   * entry. NUL terminating is not required if length is multiple of
   * LDIR_MAXLFNCHARS (13).
   */

  for (namelen = 0; dirinfo->fd_lfname[namelen] != '\0'; namelen++)
    {
      /* Empty */
    }

  DEBUGASSERT(namelen <= LDIR_MAXFNAME + 1);

  /* How many LFN directory entries do we need to write? */

  nfullentries = namelen / LDIR_MAXLFNCHARS;
  remainder    = namelen - nfullentries * LDIR_MAXLFNCHARS;
  nentries     = nfullentries;
  if (remainder > 0)
    {
      nentries++;
      remainder++;
    }

  DEBUGASSERT(nentries > 0 && nentries <= LDIR_MAXLFNS);

  /* Create the short file name alias */

  ret = fat_createalias(dirinfo);
  if (ret < 0)
    {
      return ret;
    }

  /* Set up the initial positional data */

  dirinfo->dir.fd_currcluster = dirinfo->fd_seq.ds_lfncluster;
  dirinfo->dir.fd_currsector  = dirinfo->fd_seq.ds_lfnsector;
  dirinfo->dir.fd_index       = dirinfo->fd_seq.ds_lfnoffset / DIR_SIZE;

  /* ds_lfnoffset is the offset in the sector. However fd_index is used as
   * index for the entire cluster. We need to add that offset
   */

  startsector = fat_cluster2sector(fs, dirinfo->dir.fd_currcluster);
  dirinfo->dir.fd_index += (dirinfo->dir.fd_currsector - startsector) *
                           DIRSEC_NDIRS(fs);

  /* Make sure that the alias is unique in this directory */

  ret = fat_uniquealias(fs, dirinfo);
  if (ret < 0)
    {
      return ret;
    }

  /* Get the short file name checksum */

  checksum = fat_lfnchecksum(dirinfo->fd_name);

  /* Setup the starting sequence number */

  seqno = LDIR0_LAST | nentries;

  /* Make sure that the sector containing the "last" long file name entry
   * is in the sector cache (it probably is not).
   */

  ret = fat_fscacheread(fs, dirinfo->dir.fd_currsector);
  if (ret < 0)
    {
      return ret;
    }

  /* Now loop, writing each long file name entry */

  for (; ; )
    {
      /* Get the string offset associated with the directory entry. */

      offset = (nentries - 1) * LDIR_MAXLFNCHARS;

      /* Get a reference to the current directory entry */

      diroffset = (dirinfo->dir.fd_index & DIRSEC_NDXMASK(fs)) * DIR_SIZE;
      direntry  = &fs->fs_buffer[diroffset];

      /* Is this the "last" LFN directory entry? */

      if ((seqno & LDIR0_LAST) != 0 && remainder != 0)
        {
          int nbytes;

          /* Initialize the "last" directory entry name to all 0xffff */

          fat_initlfname(LDIR_PTRWCHAR1_5(direntry), 5);
          fat_initlfname(LDIR_PTRWCHAR6_11(direntry), 6);
          fat_initlfname(LDIR_PTRWCHAR12_13(direntry), 2);

          /* Store the tail portion of the long file name in directory
           * entry.
           */

          nbytes = MIN(5, remainder);
          fat_putlfnchunk(LDIR_PTRWCHAR1_5(direntry),
                          &dirinfo->fd_lfname[offset], nbytes);
          remainder -= nbytes;

          if (remainder > 0)
            {
              nbytes = MIN(6, remainder);
              fat_putlfnchunk(LDIR_PTRWCHAR6_11(direntry),
                              &dirinfo->fd_lfname[offset + 5], nbytes);
              remainder -= nbytes;
            }

          if (remainder > 0)
            {
              nbytes = MIN(2, remainder);
              fat_putlfnchunk(LDIR_PTRWCHAR12_13(direntry),
                              &dirinfo->fd_lfname[offset + 11], nbytes);
              remainder -= nbytes;
            }

          /* The remainder should now be zero */

          DEBUGASSERT(remainder == 0);
        }
      else
        {
          /* Store a portion long file name in this directory entry  */

          fat_putlfnchunk(LDIR_PTRWCHAR1_5(direntry),
                          &dirinfo->fd_lfname[offset], 5);
          fat_putlfnchunk(LDIR_PTRWCHAR6_11(direntry),
                          &dirinfo->fd_lfname[offset + 5], 6);
          fat_putlfnchunk(LDIR_PTRWCHAR12_13(direntry),
                          &dirinfo->fd_lfname[offset + 11], 2);
        }

      /* Write the remaining directory entries */

      LDIR_PUTSEQ(direntry, seqno);
      LDIR_PUTATTRIBUTES(direntry, LDDIR_LFNATTR);
      LDIR_PUTNTRES(direntry, 0);
      LDIR_PUTCHECKSUM(direntry, checksum);
      LDIR_PUTFSTCLUSTLO(direntry, 0);
      fs->fs_dirty = true;

      /* Read next directory entry */

      if (fat_nextdirentry(fs, &dirinfo->dir) != OK)
        {
          return -ENOENT;
        }

      /* Make sure that the sector containing the directory entry is in the
       * sector cache
       */

      ret = fat_fscacheread(fs, dirinfo->dir.fd_currsector);
      if (ret < 0)
        {
          return ret;
        }

      /* Decrement the number of entries and get the next sequence number. */

      if (--nentries <= 0)
        {
          /* We have written all of the long file name entries to the media
           * and we have the short file name entry in the cache.  We can
           * just return success.
           */

          return OK;
        }

      /* The sequence number is just the number of entries left to be
       * written.
       */

      seqno = nentries;
    }
}
#endif

/****************************************************************************
 * Name: fat_putsfdirentry
 *
 * Description: Write a short file name directory entry
 *
 * Assumption:  The directory sector is in the cache.  The caller will write
 *   sector information.
 *
 ****************************************************************************/

static int fat_putsfdirentry(FAR struct fat_mountpt_s *fs,
                             FAR struct fat_dirinfo_s *dirinfo,
                             uint8_t attributes, uint32_t fattime)
{
  FAR uint8_t *direntry;

  /* Initialize the 32-byte directory entry */

  direntry = &fs->fs_buffer[dirinfo->fd_seq.ds_offset];
  memset(direntry, 0, DIR_SIZE);

  /* Directory name info */

  fat_putsfname(fs, dirinfo);

  /* Set the attribute attribute, write time, creation time */

  DIR_PUTATTRIBUTES(direntry, attributes);

  /* Set the time information */

  DIR_PUTWRTTIME(direntry, fattime & 0xffff);
  DIR_PUTCRTIME(direntry, fattime & 0xffff);
  DIR_PUTWRTDATE(direntry, fattime >> 16);
  DIR_PUTCRDATE(direntry, fattime >> 16);

  fs->fs_dirty = true;
  return OK;
}

/****************************************************************************
 * Public Functions
 ****************************************************************************/

/****************************************************************************
 * Name: fat_finddirentry
 *
 * Description: Given a path to something that may or may not be in the file
 *   system, return the description of the directory entry of the requested
 *   item.
 *
 * NOTE: As a side effect, this function returns with the sector containing
 *   the short file name directory entry in the cache.
 *
 ****************************************************************************/

int fat_finddirentry(FAR struct fat_mountpt_s *fs,
                     FAR struct fat_dirinfo_s *dirinfo,
                     FAR const char *path)
{
  FAR uint8_t *direntry;
  off_t cluster;
  char terminator;
  int ret;

  /* Initialize to traverse the chain.  Set it to the cluster of the root
   * directory
   */

  cluster = fs->fs_rootbase;
  if (fs->fs_type == FSTYPE_FAT32)
    {
      /* For FAT32, the root directory is variable sized and is a cluster
       * chain like any other directory.  fs_rootbase holds the first
       * cluster of the root directory.
       */

      dirinfo->dir.fd_startcluster = cluster;
      dirinfo->dir.fd_currcluster  = cluster;
      dirinfo->dir.fd_currsector   = fat_cluster2sector(fs, cluster);
    }
  else
    {
      /* For FAT12/16, the first sector of the root directory is a sector
       * relative to the first sector of the fat volume.
       */

      dirinfo->dir.fd_startcluster = 0;
      dirinfo->dir.fd_currcluster  = 0;
      dirinfo->dir.fd_currsector   = cluster;
    }

  /* fd_index is the index into the current directory table. It is set to the
   * the first entry in the root directory.
   */

  dirinfo->dir.fd_index = 0;

  /* If no path was provided, then the root directory must be exactly what
   * the caller is looking for.
   */

  if (*path == '\0')
    {
      dirinfo->fd_root = true;
      return OK;
    }

  /* This is not the root directory */

  dirinfo->fd_root = false;

  /* Now loop until the directory entry corresponding to the path is found */

  for (; ; )
    {
      /* Convert the next the path segment name into the kind of name that
       * we would see in the directory entry.
       */

      ret = fat_path2dirname(&path, dirinfo, &terminator);
      if (ret < 0)
        {
          /* ERROR:  The filename contains invalid characters or is
           * too long.
           */

          return ret;
        }

      /* Is this a path segment a long or a short file.  Was a long file
       * name parsed?
       */

#ifdef CONFIG_FAT_LFN
      if (dirinfo->fd_lfname[0] != '\0')
        {
          /* Yes.. Search for the sequence of long file name directory
           * entries. NOTE: As a side effect, this function returns with
           * the sector containing the short file name directory entry
           * in the cache.
           */

          ret = fat_findlfnentry(fs, dirinfo);
        }
      else
#endif
        {
          /* No.. Search for the single short file name directory entry */

          ret = fat_findsfnentry(fs, dirinfo);
        }

      /* Did we find the directory entries? */

      if (ret < 0)
        {
          /* A return value of -ENOENT would mean that the path segment
           * was not found.  Let's distinguish two cases:  (1) the final
           * file was not found in the directory (-ENOENT), or (2) one
           * of the directory path segments does not exist (-ENOTDIR)
           */

          if (ret == -ENOENT && terminator != '\0')
            {
              ret = -ENOTDIR;
            }

          return ret;
        }

      /* If the terminator character in the path was the end of the string
       * then we have successfully found the directory entry that describes
       * the path.
       */

      if (terminator == '\0')
        {
          /* Return success meaning that the description matching the
           * directory entry is in dirinfo.
           */

          return OK;
        }

      /* No.. then we have found one of the intermediate directories on
       * the way to the final path target.  In this case, make sure
       * the thing that we found is, indeed, a directory.
       */

      direntry = &fs->fs_buffer[dirinfo->fd_seq.ds_offset];
      if (!(DIR_GETATTRIBUTES(direntry) & FATATTR_DIRECTORY))
        {
          /* Ooops.. we found something else */

          return -ENOTDIR;
        }

      if (*path == '\0')
        {
          return OK;
        }

      /* Get the cluster number of this directory */

      cluster =
          ((uint32_t)DIR_GETFSTCLUSTHI(direntry) << 16) |
          DIR_GETFSTCLUSTLO(direntry);

      /* Then restart scanning at the new directory, skipping over both the
       * '.' and '..' entries that exist in all directories EXCEPT the root
       * directory.
       */

      dirinfo->dir.fd_startcluster = cluster;
      dirinfo->dir.fd_currcluster  = cluster;
      dirinfo->dir.fd_currsector   = fat_cluster2sector(fs, cluster);
      dirinfo->dir.fd_index        = 2;
    }
}

/****************************************************************************
 * Name: fat_allocatedirentry
 *
 * Description:
 *   Find (or allocate) all needed directory entries to contain the file name
 *
 ****************************************************************************/

int fat_allocatedirentry(FAR struct fat_mountpt_s *fs,
                         FAR struct fat_dirinfo_s *dirinfo)
{
  int32_t cluster;
  int32_t prevcluster;
  off_t   sector;
  int     ret;
  int     i;

  /* Re-initialize directory object */

  cluster = dirinfo->dir.fd_startcluster;

  /* Loop until we successfully allocate the sequence of directory entries
   * or until to fail to extend the directory cluster chain.
   */

  for (; ; )
    {
      /* Can this cluster chain be extended */

      if (cluster)
        {
          /* Cluster chain can be extended */

          dirinfo->dir.fd_currcluster = cluster;
          dirinfo->dir.fd_currsector  = fat_cluster2sector(fs, cluster);
        }
      else
        {
          /* Fixed size FAT12/16 root directory is at fixed offset/size */

          dirinfo->dir.fd_currsector = fs->fs_rootbase;
        }

      /* Start at the first entry in the root directory. */

      dirinfo->dir.fd_index = 0;

      /* Is this a path segment a long or a short file.  Was a long file
       * name parsed?
       */

#ifdef CONFIG_FAT_LFN
      if (dirinfo->fd_lfname[0] != '\0')
        {
          /* Yes.. Allocate for the sequence of long file name directory
           * entries plus a short file name directory entry.
           */

          ret = fat_allocatelfnentry(fs, dirinfo);
        }

      /* No.. Allocate only a short file name directory entry */

      else
#endif
        {
          ret = fat_allocatesfnentry(fs, dirinfo);
        }

      /* Did we successfully allocate the directory entries?  If the error
       * value is -ENOSPC, then we can try to extend the directory cluster
       * (we can't handle other return values)
       */

      if (ret == OK || ret != -ENOSPC)
        {
          return ret;
        }

      /* If we get here, then we have reached the end of the directory table
       * in this sector without finding a free directory entry.
       *
       * It this is a fixed size directory entry, then this is an error.
       * Otherwise, we can try to extend the directory cluster chain to
       * make space for the new directory entry.
       */

      if (!cluster)
        {
          /* The size is fixed */

          return -ENOSPC;
        }

      /* Try to extend the cluster chain for this directory */

      prevcluster = cluster;
      cluster     = fat_extendchain(fs, dirinfo->dir.fd_currcluster);

      if (cluster < 0)
        {
          return cluster;
        }

      /* Flush out any cached data in fs_buffer.. we are going to use
       * it to initialize the new directory cluster.
       */

      ret = fat_fscacheflush(fs);
      if (ret < 0)
        {
          return ret;
        }

      /* Clear all sectors comprising the new directory cluster */

      fs->fs_currentsector = fat_cluster2sector(fs, cluster);
      memset(fs->fs_buffer, 0, fs->fs_hwsectorsize);

      sector = fs->fs_currentsector;
      for (i = fs->fs_fatsecperclus; i; i--)
        {
          ret = fat_hwwrite(fs, fs->fs_buffer, sector, 1);
          if (ret < 0)
            {
              return ret;
            }

          sector++;
        }

      /* Start the search again */

      cluster = prevcluster;
    }
}

/****************************************************************************
 * Name: fat_freedirentry
 *
 * Description:  Free the directory entry.
 *
 * NOTE: As a side effect, this function returns with the sector containing
 *   the deleted short file name directory entry in the cache.
 *
 ****************************************************************************/

int fat_freedirentry(FAR struct fat_mountpt_s *fs, struct fat_dirseq_s *seq)
{
#ifdef CONFIG_FAT_LFN
  struct fs_fatdir_s dir;
  FAR uint8_t *direntry;
  uint16_t diroffset;
  off_t startsector;
  int ret;

  /* Set it to the cluster containing the "last" LFN entry (that appears
   * first on the media).
   */

  dir.fd_currcluster = seq->ds_lfncluster;
  dir.fd_currsector  = seq->ds_lfnsector;
  dir.fd_index       = seq->ds_lfnoffset / DIR_SIZE;

  /* Remember that ds_lfnoffset is the offset in the sector and not the
   * cluster.
   */

  startsector        = fat_cluster2sector(fs, dir.fd_currcluster);
  dir.fd_index      += (dir.fd_currsector - startsector) * DIRSEC_NDIRS(fs);

  /* Free all of the directory entries used for the sequence of long file
   * name and for the single short file name entry.
   */

  for (; ; )
    {
      /* Read the directory sector into the sector cache */

      ret = fat_fscacheread(fs, dir.fd_currsector);
      if (ret < 0)
        {
          return ret;
        }

      /* Get a pointer to the directory entry */

      diroffset = (dir.fd_index & DIRSEC_NDXMASK(fs)) * DIR_SIZE;
      direntry  = &fs->fs_buffer[diroffset];

      /* Then mark the entry as deleted */

      direntry[DIR_NAME] = DIR0_EMPTY;
      fs->fs_dirty       = true;

      /* Did we just free the single short file name entry? */

      if (dir.fd_currsector == seq->ds_sector &&
          diroffset == seq->ds_offset)
        {
          /* Yes.. then we are finished. flush anything remaining in the
           * cache and return, probably successfully.
           */

          return fat_fscacheflush(fs);
        }

      /* There are more entries to go.. Try the next directory entry */

      ret = fat_nextdirentry(fs, &dir);
      if (ret < 0)
        {
          return ret;
        }
    }

#else
  FAR uint8_t *direntry;
  int ret;

  /* Free the single short file name entry.
   *
   * Make sure that the sector containing the directory entry is in the
   * cache.
   */

  ret = fat_fscacheread(fs, seq->ds_sector);
  if (ret == OK)
    {
      /* Then mark the entry as deleted */

      direntry           = &fs->fs_buffer[seq->ds_offset];
      direntry[DIR_NAME] = DIR0_EMPTY;
      fs->fs_dirty       = true;
    }

  return ret;
#endif
}

/****************************************************************************
 * Name: fat_dirname2path
 *
 * Description:  Convert a filename in a raw directory entry into a user
 *    filename.  This is essentially the inverse operation of that performed
 *    by fat_path2dirname.  See that function for more details.
 *
 ****************************************************************************/

int fat_dirname2path(FAR struct fat_mountpt_s *fs,
                     FAR struct fs_dirent_s *dir,
                     FAR struct dirent *entry)
{
  FAR struct fat_dirent_s *fdir;
  uint16_t diroffset;
  FAR uint8_t *direntry;
#ifdef CONFIG_FAT_LFN
  uint8_t attribute;
#endif

  /* Get a reference to the current directory entry */

  fdir = (FAR struct fat_dirent_s *)dir;
  diroffset = (fdir->dir.fd_index & DIRSEC_NDXMASK(fs)) * DIR_SIZE;
  direntry = &fs->fs_buffer[diroffset];

  /* Does this entry refer to the last entry of a long file name? */

#ifdef CONFIG_FAT_LFN
  attribute = DIR_GETATTRIBUTES(direntry);
  if (((*direntry & LDIR0_LAST) != 0 && attribute == LDDIR_LFNATTR))
    {
      /* Yes.. Get the name from a sequence of long file name directory
       * entries.
       */

      return fat_getlfname(fs, dir, entry);
    }
  else
#endif
    {
      /* No.. Get the name from a short file name directory entries */

      return fat_getsfname(direntry, entry->d_name, NAME_MAX + 1);
    }
}

/****************************************************************************
 * Name: fat_dirnamewrite
 *
 * Description:
 *   Write the (possibly long) directory entry name.  This function is
 *   called only from fat_rename to write the new file name.
 *
 * Assumption:
 *   The directory sector containing the short file name entry is in the
 *  cache.  *NOT* the sector containing the last long file name entry!
 *
 ****************************************************************************/

int fat_dirnamewrite(FAR struct fat_mountpt_s *fs,
                     FAR struct fat_dirinfo_s *dirinfo)
{
#ifdef CONFIG_FAT_LFN
  int ret;

  /* Is this a long file name? */

  if (dirinfo->fd_lfname[0] != '\0')
    {
      /* Write the sequence of long file name directory entries (this
       * function also creates the short file name alias).
       */

      ret = fat_putlfname(fs, dirinfo);
      if (ret != OK)
        {
          return ret;
        }
    }

  /* On return, fat_lfsfname() will leave the short file name entry in the
   * cache.  So we can just fall through to write that directory entry,
   * perhaps using the short file name alias for the long file name.
   */

#endif

  return fat_putsfname(fs, dirinfo);
}

/****************************************************************************
 * Name: fat_dirwrite
 *
 * Description: Write a directory entry, possibly with a long file name.
 *   Called from:
 *
 *   fat_mkdir() to write the new FAT directory entry.
 *   fat_dircreate() to create any new directory entry.
 *
 * Assumption:  The directory sector is in the cache.  The caller will write
 *   sector information.
 *
 ****************************************************************************/

int fat_dirwrite(FAR struct fat_mountpt_s *fs,
                 FAR struct fat_dirinfo_s *dirinfo,
                 uint8_t attributes, uint32_t fattime)
{
#ifdef CONFIG_FAT_LFN
  int ret;

  /* Does this directory entry have a long file name? */

  if (dirinfo->fd_lfname[0] != '\0')
    {
      /* Write the sequence of long file name directory entries (this
       * function also creates the short file name alias).
       */

      ret = fat_putlfname(fs, dirinfo);
      if (ret != OK)
        {
          return ret;
        }
    }

  /* On return, fat_lfsfname() will leave the short file name entry in the
   * cache.  So we can just fall through to write that directory entry,
   * perhaps using the short file name alias for the long file name.
   */

#endif

  /* Put the short file name entry data */

  return fat_putsfdirentry(fs, dirinfo, attributes, fattime);
}

/****************************************************************************
 * Name: fat_dircreate
 *
 * Description: Create a directory entry for a new file
 *
 ****************************************************************************/

int fat_dircreate(FAR struct fat_mountpt_s *fs,
                  FAR struct fat_dirinfo_s *dirinfo)
{
  uint32_t fattime;
  int ret;

  /* Allocate a directory entry.  If long file name support is enabled, then
   * this might, in fact, allocate a sequence of directory entries.
   */

  ret = fat_allocatedirentry(fs, dirinfo);
  if (ret != OK)
    {
      /* Failed to allocate the required directory entry or entries. */

      return ret;
    }

  /* Write the directory entry (or entries) with the current time and the
   * ARCHIVE attribute.
   */

  fattime = fat_systime2fattime();
  return fat_dirwrite(fs, dirinfo, FATATTR_ARCHIVE, fattime);
}

/****************************************************************************
 * Name: fat_remove
 *
 * Description: Remove a directory or file from the file system.  This
 *   implements both rmdir() and unlink().
 *
 ****************************************************************************/

int fat_remove(FAR struct fat_mountpt_s *fs, FAR const char *relpath,
               bool directory)
{
  struct fat_dirinfo_s dirinfo;
  uint32_t             dircluster;
  FAR uint8_t         *direntry;
  int                  ret;

  /* Find the directory entry referring to the entry to be deleted */

  ret = fat_finddirentry(fs, &dirinfo, relpath);
  if (ret != OK)
    {
      /* Most likely, some element of the path does not exist. */

      return -ENOENT;
    }

  /* Check if this is a FAT12/16 root directory */

  if (dirinfo.fd_root)
    {
      /* The root directory cannot be removed */

      return -EPERM;
    }

  /* The object has to have write access to be deleted */

  direntry = &fs->fs_buffer[dirinfo.fd_seq.ds_offset];
  if ((DIR_GETATTRIBUTES(direntry) & FATATTR_READONLY) != 0)
    {
      /* It is a read-only entry */

      return -EACCES;
    }

  /* Get the directory sector and cluster containing the entry to be
   * deleted.
   */

  dircluster =
      ((uint32_t)DIR_GETFSTCLUSTHI(direntry) << 16) |
      DIR_GETFSTCLUSTLO(direntry);

  /* Is this entry a directory? */

  if (DIR_GETATTRIBUTES(direntry) & FATATTR_DIRECTORY)
    {
      /* It is a sub-directory. Check if we are be asked to remove
       * a directory or a file.
       */

      if (!directory)
        {
          /* We are asked to delete a file */

          return -EISDIR;
        }

      /* We are asked to delete a directory. Check if this sub-directory is
       * empty (i.e., that there are no valid entries other than the initial
       * '.' and '..' entries).
       */

      dirinfo.dir.fd_currcluster = dircluster;
      dirinfo.dir.fd_currsector  = fat_cluster2sector(fs, dircluster);
      dirinfo.dir.fd_index       = 2;

      /* Loop until either (1) an entry is found in the directory (error),
       * (2) the directory is found to be empty, or (3) some error occurs.
       */

      for (; ; )
        {
          unsigned int subdirindex;
          uint8_t     *subdirentry;

          /* Make sure that the sector containing the of the subdirectory
           * sector is in the cache
           */

          ret = fat_fscacheread(fs, dirinfo.dir.fd_currsector);
          if (ret < 0)
            {
              return ret;
            }

          /* Get a reference to the next entry in the directory */

          subdirindex = (dirinfo.dir.fd_index & DIRSEC_NDXMASK(fs)) *
                        DIR_SIZE;
          subdirentry = &fs->fs_buffer[subdirindex];

          /* Is this the last entry in the directory? */

          if (subdirentry[DIR_NAME] == DIR0_ALLEMPTY)
            {
              /* Yes then the directory is empty.  Break out of the
               * loop and delete the directory.
               */

              break;
            }

          /* Check if the next entry refers to a file or directory */

          if (subdirentry[DIR_NAME] != DIR0_EMPTY &&
              !(DIR_GETATTRIBUTES(subdirentry) & FATATTR_VOLUMEID))
            {
              /* The directory is not empty */

              return -ENOTEMPTY;
            }

          /* Get the next directory entry */

          ret = fat_nextdirentry(fs, &dirinfo.dir);
          if (ret < 0)
            {
              return ret;
            }
        }
    }
  else
    {
      /* It is a file. Check if we are be asked to remove a directory
       * or a file.
       */

      if (directory)
        {
          /* We are asked to remove a directory */

          return -ENOTDIR;
        }
    }

  /* Mark the directory entry 'deleted'.  If long file name support is
   * enabled, then multiple directory entries may be freed.
   */

  ret = fat_freedirentry(fs, &dirinfo.fd_seq);
  if (ret < 0)
    {
      return ret;
    }

  /* And remove the cluster chain making up the subdirectory */

  ret = fat_removechain(fs, dircluster);
  if (ret < 0)
    {
      return ret;
    }

  /* Update the FSINFO sector (FAT32) */

  ret = fat_updatefsinfo(fs);
  if (ret < 0)
    {
      return ret;
    }

  return OK;
}