text: /**
   * Copyright (c) 2011-2020 Bill Greiman
   * This file is part of the SdFat library for SD memory cards.
   *
   * MIT License
   *
   * Permission is hereby granted, free of charge, to any person obtaining a
   * copy of this software and associated documentation files (the "Software"),
   * to deal in the Software without restriction, including without limitation
   * the rights to use, copy, modify, merge, publish, distribute, sublicense,
   * and/or sell copies of the Software, and to permit persons to whom the
   * Software is furnished to do so, subject to the following conditions:
   *
   * The above copyright notice and this permission notice shall be included
   * in all copies or substantial portions of the Software.
   *
   * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
   * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
   * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
   * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
   * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
   * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
   * DEALINGS IN THE SOFTWARE.
   */
  #define DBG_FILE "ExFatFile.cpp"
  #include "../common/DebugMacros.h"
  #include "ExFatFile.h"
  #include "ExFatVolume.h"
  #include "upcase.h"
  //-----------------------------------------------------------------------------
  bool ExFatFile::close() {
    bool rtn = sync();
    m_attributes = FILE_ATTR_CLOSED;
    m_flags = 0;
    return rtn;
  }
  //------------------------------------------------------------------------------
  void ExFatFile::fgetpos(fspos_t* pos) {
    pos->position = m_curPosition;
    pos->cluster = m_curCluster;
  }
  //------------------------------------------------------------------------------
  int ExFatFile::fgets(char* str, int num, char* delim) {
    char ch;
    int n = 0;
    int r = -1;
    while ((n + 1) < num && (r = read(&ch, 1)) == 1) {
      // delete CR
      if (ch == '\r') {
        continue;
      }
      str[n++] = ch;
      if (!delim) {
        if (ch == '\n') {
          break;
        }
      } else {
        if (strchr(delim, ch)) {
          break;
        }
      }
    }
    if (r < 0) {
      // read error
      return -1;
    }
    str[n] = '\0';
    return n;
  }
  //------------------------------------------------------------------------------
  void ExFatFile::fsetpos(const fspos_t* pos) {
    m_curPosition = pos->position;
    m_curCluster = pos->cluster;
  }
  //-----------------------------------------------------------------------------
  size_t ExFatFile::getName(ExChar_t *name, size_t length) {
    DirName_t* dn;
    DirPos_t pos = m_dirPos;
    size_t n = 0;
    if (!isOpen()) {
        DBG_FAIL_MACRO;
        goto fail;
    }
    for (uint8_t is = 1; is < m_setCount; is++) {
      if (m_vol->dirSeek(&pos, is == 1 ? 64: 32) != 1) {
        DBG_FAIL_MACRO;
        goto fail;
      }
      dn = reinterpret_cast<DirName_t*>
           (m_vol->dirCache(&pos, FsCache::CACHE_FOR_READ));
      if (!dn || dn->type != EXFAT_TYPE_NAME) {
        DBG_FAIL_MACRO;
        goto fail;
      }
      for (uint8_t in = 0; in < 15; in++) {
        if ((n + 1) >= length) {
          goto done;
        }
        uint16_t c = getLe16(dn->unicode + 2*in);
        name[n++] = sizeof(ExChar_t) > 1 || c < 0X7F ? c : '?';
      }
    }
   done:
    name[n] = 0;
    return n;
  
   fail:
    *name = 0;
    return 0;
  }
  //-----------------------------------------------------------------------------
  bool ExFatFile::open(const ExChar_t* path, int oflag) {
    return open(ExFatVolume::cwv(), path, oflag);
  }
  //-----------------------------------------------------------------------------
  bool ExFatFile::open(ExFatVolume* vol, const ExChar_t* path, int oflag) {
    return vol && open(vol->vwd(), path, oflag);
  }
  //------------------------------------------------------------------------------
  bool ExFatFile::open(ExFatFile* dirFile, const ExChar_t* path, oflag_t oflag) {
    ExFatFile tmpDir;
    ExName_t fname;
    // error if already open
    if (isOpen() || !dirFile->isDir()) {
      DBG_FAIL_MACRO;
      goto fail;
    }
    if (isDirSeparator(*path)) {
      while (isDirSeparator(*path)) {
        path++;
      }
      if (*path == 0) {
        return openRoot(dirFile->m_vol);
      }
      if (!tmpDir.openRoot(dirFile->m_vol)) {
        DBG_FAIL_MACRO;
        goto fail;
      }
      dirFile = &tmpDir;
    }
    while (1) {
      if (!parsePathName(path, &fname, &path)) {
        DBG_FAIL_MACRO;
        goto fail;
      }
      if (*path == 0) {
        break;
      }
      if (!open(dirFile, &fname, O_RDONLY)) {
        DBG_FAIL_MACRO;
        goto fail;
      }
      tmpDir = *this;
      dirFile = &tmpDir;
      close();
    }
    return open(dirFile, &fname, oflag);
  
  fail:
    return false;
  }
  //------------------------------------------------------------------------------
  bool ExFatFile::open(ExFatFile* dirFile, uint32_t index, oflag_t oflag) {
    if (dirFile->seekSet(32*index) && openNext(dirFile, oflag)) {
      if (dirIndex() == index) {
        return true;
      }
      close();
      DBG_FAIL_MACRO;
    }
    return false;
  }
  //-----------------------------------------------------------------------------
  bool ExFatFile::openNext(ExFatFile* dir, oflag_t oflag) {
    if (isOpen() || !dir->isDir() || (dir->curPosition() & 0X1F)) {
      DBG_FAIL_MACRO;
      goto fail;
    }
    return openRootFile(dir, nullptr, 0, oflag);
  
   fail:
    return false;
  }
  //------------------------------------------------------------------------------
  bool ExFatFile::openRootFile(ExFatFile* dir, const ExChar_t* name,
                            uint8_t nameLength, oflag_t oflag) {
    int n;
    uint8_t nameOffset = 0;
    uint8_t nCmp;
    uint8_t modeFlags;
    uint16_t nameHash = 0;
    uint32_t curCluster __attribute__((unused));
    uint8_t* cache __attribute__((unused));
    DirPos_t freePos __attribute__((unused));
  
    DirFile_t*   dirFile;
    DirStream_t* dirStream;
    DirName_t*   dirName;
    uint8_t buf[32];
    uint8_t freeCount = 0;
    uint8_t freeNeed;
    bool inSet = false;
  
    // error if already open
    if (isOpen() || !dir->isDir()) {
      DBG_FAIL_MACRO;
      goto fail;
    }
    switch (oflag & O_ACCMODE) {
      case O_RDONLY:
        modeFlags = FILE_FLAG_READ;
        break;
      case O_WRONLY:
        modeFlags = FILE_FLAG_WRITE;
        break;
      case O_RDWR:
        modeFlags = FILE_FLAG_READ | FILE_FLAG_WRITE;
        break;
      default:
        DBG_FAIL_MACRO;
        goto fail;
    }
    modeFlags |= oflag & O_APPEND ? FILE_FLAG_APPEND : 0;
    if (name) {
      nameHash = exFatHashName(name, nameLength, 0);
      dir->rewind();
    }
    freeNeed = 2 + (nameLength + 14)/15;
  
    while (1) {
      n = dir->read(buf, 32);
      if (n == 0) {
        goto create;
      }
      if (n != 32) {
        DBG_FAIL_MACRO;
        goto fail;
      }
      if (!(buf[0] & 0x80)) {
        if (freeCount == 0) {
          freePos.position = dir->curPosition() - 32;
          freePos.cluster = dir->curCluster();
        }
        if (freeCount < freeNeed) {
          freeCount++;
        }
        if (!buf[0]) {
          goto create;
        }
      } else if (!inSet) {
        if (freeCount < freeNeed) {
          freeCount = 0;
        }
        if (buf[0] != EXFAT_TYPE_FILE) {
          continue;
        }
        inSet = true;
      }
      switch (buf[0]) {
        case EXFAT_TYPE_FILE:
          memset(this, 0, sizeof(ExFatFile));
          dirFile = reinterpret_cast<DirFile_t*>(buf);
          m_setCount = dirFile->setCount;
          m_attributes = getLe16(dirFile->attributes) & FILE_ATTR_COPY;
          if (!(m_attributes & EXFAT_ATTRIB_DIRECTORY)) {
            m_attributes |= FILE_ATTR_FILE;
          }
          m_vol = dir->volume();
  
          m_dirPos.cluster = dir->curCluster();
          m_dirPos.position = dir->curPosition() - 32;
          m_dirPos.isContiguous = dir->isContiguous();
          break;
  
        case EXFAT_TYPE_STREAM:
          dirStream = reinterpret_cast<DirStream_t*>(buf);
          m_flags = modeFlags;
          if (dirStream->flags & EXFAT_FLAG_CONTIGUOUS) {
            m_flags |= FILE_FLAG_CONTIGUOUS;
          }
          nameOffset = 0;
          m_validLength = getLe64(dirStream->validLength);
          m_firstCluster = getLe32(dirStream->firstCluster);
          m_dataLength = getLe64(dirStream->dataLength);
          if (!name) {
            goto found;
          }
          if (nameLength != dirStream->nameLength ||
              nameHash != getLe16(dirStream->nameHash)) {
            inSet = false;
            break;
          }
          break;
  
        case EXFAT_TYPE_NAME:
          dirName = reinterpret_cast<DirName_t*>(buf);
          nCmp = nameLength - nameOffset;
          if (nCmp > 15) {
            nCmp = 15;
          }
          if (!exFatCmpName(dirName, name, nameOffset, nCmp)) {
            inSet = false;
            break;
          }
          nameOffset += nCmp;
  
          if (nameOffset == nameLength) {
            goto found;
          }
          break;
  
        default:
          break;
      }
    }
  
   found:
    // Don't open if create only.
    if (oflag & O_EXCL) {
      DBG_FAIL_MACRO;
      goto fail;
    }
    // Write, truncate, or at end is an error for a directory or read-only file.
    if ((oflag & (O_TRUNC | O_AT_END)) || (m_flags & FILE_FLAG_WRITE)) {
      if (isSubDir() || isReadOnly() || READ_ONLY) {
        DBG_FAIL_MACRO;
        goto fail;
      }
    }
  
  #if !READ_ONLY
    if (oflag & O_TRUNC) {
      if (!(m_flags & FILE_FLAG_WRITE)) {
        DBG_FAIL_MACRO;
        goto fail;
      }
      if (!truncate(0)) {
        DBG_FAIL_MACRO;
        goto fail;
      }
    } else if ((oflag & O_AT_END) && !seekSet(fileSize())) {
      DBG_FAIL_MACRO;
      goto fail;
    }
  #endif  // READ_ONLY
    return true;
  
   create:
  #if READ_ONLY
    DBG_FAIL_MACRO;
    goto fail;
  #else  // READ_ONLY
    // don't create unless O_CREAT and write
    if (!(oflag & O_CREAT) || !(modeFlags & FILE_FLAG_WRITE) || !name) {
      DBG_FAIL_MACRO;
      goto fail;
    }
    while (freeCount < freeNeed) {
      n = dir->read(buf, 32);
      if (n == 0) {
        curCluster = dir->m_curCluster;
        if (!dir->addDirCluster()) {
          DBG_FAIL_MACRO;
          goto fail;
        }
        dir->m_curCluster = curCluster;
        continue;
      }
      if (n != 32) {
        DBG_FAIL_MACRO;
        goto fail;
      }
      if (freeCount == 0) {
        freePos.position = dir->curPosition() - 32;
        freePos.cluster = dir->curCluster();
      }
      freeCount++;
    }
  
    freePos.isContiguous = dir->isContiguous();
    memset(this, 0, sizeof(ExFatFile));
    m_vol = dir->volume();
    m_attributes = FILE_ATTR_FILE;
    m_dirPos = freePos;
    for (uint8_t i = 0; i < freeNeed; i++) {
      if (i) {
        if (1 != m_vol->dirSeek(&freePos, 32)) {
          DBG_FAIL_MACRO;
          goto fail;
        }
      }
      cache = m_vol->dirCache(&freePos, FsCache::CACHE_FOR_WRITE);
      if (!cache || (cache[0] & 0x80)) {
        DBG_FAIL_MACRO;
        goto fail;
      }
      memset(cache, 0 , 32);
      if (i == 0) {
        dirFile = reinterpret_cast<DirFile_t*>(cache);
        dirFile->type = EXFAT_TYPE_FILE;
        m_setCount = freeNeed - 1;
        dirFile->setCount = m_setCount;
        if (FsDateTime::callback) {
          uint16_t date, time;
          uint8_t ms10;
          FsDateTime::callback(&date, &time, &ms10);
          dirFile->createTimeMs = ms10;
          setLe16(dirFile->createTime, time);
          setLe16(dirFile->createDate, date);
        }
      } else if (i == 1) {
        dirStream = reinterpret_cast<DirStream_t*>(cache);
        dirStream->type = EXFAT_TYPE_STREAM;
        dirStream->flags = EXFAT_FLAG_ALWAYS1 | EXFAT_FLAG_CONTIGUOUS;
        m_flags = modeFlags | FILE_FLAG_CONTIGUOUS | FILE_FLAG_DIR_DIRTY;
  
        dirStream->nameLength = nameLength;
        setLe16(dirStream->nameHash, nameHash);
      } else {
        dirName = reinterpret_cast<DirName_t*>(cache);
        dirName->type = EXFAT_TYPE_NAME;
        nameOffset = 15*(i - 2);
        nCmp = nameLength - nameOffset;
        if (nCmp > 15) {
          nCmp = 15;
        }
        for (size_t k = 0; k < nCmp; k++) {
          setLe16(dirName->unicode + 2*k, name[k + nameOffset]);
        }
      }
    }
    return sync();
  #endif  // READ_ONLY
   fail:
  
    // close file
    m_attributes = FILE_ATTR_CLOSED;
    m_flags = 0;
    return false;
  }
  //-----------------------------------------------------------------------------
  bool ExFatFile::openRoot(ExFatVolume* vol) {
    if (isOpen()) {
      DBG_FAIL_MACRO;
      goto fail;
    }
    memset(this, 0, sizeof(ExFatFile));
    m_attributes = FILE_ATTR_ROOT;
    m_vol = vol;
    m_flags = FILE_FLAG_READ;
    return true;
  
   fail:
    return false;
  }
  //------------------------------------------------------------------------------
  bool ExFatFile::parsePathName(const ExChar_t* path,
                              ExName_t* fname, const ExChar_t** ptr) {
    ExChar_t c;
    int end;
    int len = 0;
  
    // Skip leading spaces.
    while (*path == ' ') {
      path++;
    }
    fname->lfn = path;
  
    for (len = 0; ; len++) {
      c = path[len];
      if (c == 0 || isDirSeparator(c)) {
        break;
      }
      if (!lfnLegalChar(c)) {
        return false;
      }
    }
    // Advance to next path component.
    for (end = len; path[end] ==  ' ' || isDirSeparator(path[end]); end++) {}
    *ptr = &path[end];
  
    // Back over spaces and dots.
    while (len) {
      c = path[len - 1];
      if (c != '.' && c != ' ') {
        break;
      }
      len--;
    }
    // Max length of LFN is 255.
    if (len > EXFAT_MAX_NAME_LENGTH) {
      return false;
    }
    fname->len = len;
    return true;
  }
  //-----------------------------------------------------------------------------
  int ExFatFile::peek() {
    uint64_t curPosition = m_curPosition;
    uint32_t curCluster = m_curCluster;
    int c = read();
    m_curPosition = curPosition;
    m_curCluster = curCluster;
    return c;
  }
  //-----------------------------------------------------------------------------
  size_t ExFatFile::printName(print_t* pr) {
    DirName_t* dn;
    DirPos_t pos = m_dirPos;
    size_t n = 0;
    uint8_t in;
    uint8_t buf[15];
    if (!isOpen()) {
        DBG_FAIL_MACRO;
        goto fail;
    }
    for (uint8_t is = 1; is < m_setCount; is++) {
      if (m_vol->dirSeek(&pos, is == 1 ? 64: 32) != 1) {
        DBG_FAIL_MACRO;
        goto fail;
      }
      dn = reinterpret_cast<DirName_t*>
           (m_vol->dirCache(&pos, FsCache::CACHE_FOR_READ));
      if (!dn || dn->type != EXFAT_TYPE_NAME) {
        DBG_FAIL_MACRO;
        goto fail;
      }
      for (in = 0; in < 15; in++) {
        uint16_t c = getLe16(dn->unicode + 2*in);
        if (!c) {
          break;;
        }
        buf[in] = c < 0X7f ? c : '?';
        n++;
      }
      pr->write(buf, in);
    }
    return n;
  
   fail:
    return 0;
  }
  //-----------------------------------------------------------------------------
  int ExFatFile::read(void* buf, size_t count) {
    uint8_t* dst = reinterpret_cast<uint8_t*>(buf);
    int8_t fg;
    size_t toRead = count;
    size_t n;
    uint8_t* cache;
    uint16_t sectorOffset;
    uint32_t sector;
    uint32_t clusterOffset;
  
    if (!isReadable()) {
      DBG_FAIL_MACRO;
      goto fail;
    }
    if (isContiguous() || isFile()) {
      if ((m_curPosition + count) > m_validLength) {
        count = toRead = m_validLength - m_curPosition;
      }
    }
    while (toRead) {
      clusterOffset = m_curPosition & m_vol->clusterMask();
      sectorOffset = clusterOffset & m_vol->sectorMask();
      if (clusterOffset == 0) {
        if (m_curPosition == 0) {
          m_curCluster = isRoot()
                         ? m_vol->rootDirectoryCluster() : m_firstCluster;
        } else if (isContiguous()) {
          m_curCluster++;
        } else {
          fg = m_vol->fatGet(m_curCluster, &m_curCluster);
          if (fg < 0) {
            DBG_FAIL_MACRO;
            goto fail;
          }
          if (fg == 0) {
            // EOF if directory.
            if (isDir()) {
              break;
            }
            DBG_FAIL_MACRO;
            goto fail;
          }
        }
      }
      sector = m_vol->clusterStartSector(m_curCluster) +
               (clusterOffset >> m_vol->bytesPerSectorShift());
      if (sectorOffset != 0 || toRead < m_vol->bytesPerSector()
                            || sector == m_vol->dataCacheSector()) {
        n = m_vol->bytesPerSector() - sectorOffset;
        if (n > toRead) {
          n = toRead;
        }
        // read sector to cache and copy data to caller
        cache = m_vol->dataCacheGet(sector, FsCache::CACHE_FOR_READ);
        if (!cache) {
          DBG_FAIL_MACRO;
          goto fail;
        }
        uint8_t* src = cache + sectorOffset;
        memcpy(dst, src, n);
  #if USE_MULTI_SECTOR_IO
      } else if (toRead >= 2*m_vol->bytesPerSector()) {
        uint32_t ns = toRead >> m_vol->bytesPerSectorShift();
        // Limit writes to current cluster.
        uint32_t maxNs = m_vol->sectorsPerCluster()
                         - (clusterOffset >> m_vol->bytesPerSectorShift());
        if (ns > maxNs) {
          ns = maxNs;
        }
        n = ns << m_vol->bytesPerSectorShift();
        // Check for cache sector in read range.
        if (sector <= m_vol->dataCacheSector()
            && m_vol->dataCacheSector() < (sector + ns)) {
          // Flush cache if cache sector is in the range.
          if (!m_vol->dataCacheSync()) {
            DBG_FAIL_MACRO;
            goto fail;
          }
        }
        if (!m_vol->readSectors(sector, dst, ns)) {
          DBG_FAIL_MACRO;
          goto fail;
        }
  #endif  // USE_MULTI_SECTOR_IO
      } else {
        // read single sector
        n = m_vol->bytesPerSector();
        if (!m_vol->readSector(sector, dst)) {
          DBG_FAIL_MACRO;
          goto fail;
        }
      }
      dst += n;
      m_curPosition += n;
      toRead -= n;
    }
    return count - toRead;
  
  fail:
    m_error |= READ_ERROR;
    return -1;
  }
  //------------------------------------------------------------------------------
  bool ExFatFile::remove(const ExChar_t* path) {
    ExFatFile file;
    if (!file.open(this, path, O_WRONLY)) {
      DBG_FAIL_MACRO;
      goto fail;
    }
    return file.remove();
  
  fail:
    return false;
  }
  //------------------------------------------------------------------------------
  bool ExFatFile::seekSet(uint64_t pos) {
    uint32_t nCur;
    uint32_t nNew;
    uint32_t tmp = m_curCluster;
    // error if file not open
    if (!isOpen()) {
      DBG_FAIL_MACRO;
      goto fail;
    }
    // Optimize O_APPEND writes.
    if (pos == m_curPosition) {
      return true;
    }
    if (pos == 0) {
      // set position to start of file
      m_curCluster = 0;
      goto done;
    }
    if (isFile()) {
      if (pos > m_validLength) {
        DBG_FAIL_MACRO;
        goto fail;
      }
    }
    // calculate cluster index for new position
    nNew = (pos - 1) >> m_vol->bytesPerClusterShift();
    if (isContiguous()) {
      m_curCluster = m_firstCluster + nNew;
      goto done;
    }
    // calculate cluster index for current position
    nCur = (m_curPosition - 1) >> m_vol->bytesPerClusterShift();
    if (nNew < nCur || m_curPosition == 0) {
      // must follow chain from first cluster
      m_curCluster = isRoot() ? m_vol->rootDirectoryCluster() : m_firstCluster;
    } else {
      // advance from curPosition
      nNew -= nCur;
    }
    while (nNew--) {
      if (m_vol->fatGet(m_curCluster, &m_curCluster) <= 0) {
        DBG_FAIL_MACRO;
        goto fail;
      }
    }
  
  done:
    m_curPosition = pos;
    return true;
  
  fail:
    m_curCluster = tmp;
    return false;
  }
  