/** * Copyright (c) 2011-2022 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 "../common/FsUtf.h" #include "ExFatLib.h" //------------------------------------------------------------------------------ /** test for legal character. * * \param[in] c character to be tested. * * \return true for legal character else false. */ inline bool lfnLegalChar(uint8_t c) { #if USE_UTF8_LONG_NAMES return !lfnReservedChar(c); #else // USE_UTF8_LONG_NAMES return !(lfnReservedChar(c) || c & 0X80); #endif // USE_UTF8_LONG_NAMES } //------------------------------------------------------------------------------ bool ExFatFile::attrib(uint8_t bits) { if (!isFileOrSubDir() || (bits & FS_ATTRIB_USER_SETTABLE) != bits) { DBG_FAIL_MACRO; goto fail; } // Don't allow read-only to be set if the file is open for write. if ((bits & FS_ATTRIB_READ_ONLY) && isWritable()) { DBG_FAIL_MACRO; goto fail; } m_attributes = (m_attributes & ~FS_ATTRIB_USER_SETTABLE) | bits; // insure sync() will update dir entry m_flags |= FILE_FLAG_DIR_DIRTY; if (!sync()) { DBG_FAIL_MACRO; goto fail; } return true; fail: return false; } //------------------------------------------------------------------------------ uint8_t* ExFatFile::dirCache(uint8_t set, uint8_t options) { DirPos_t pos = m_dirPos; if (m_vol->dirSeek(&pos, FS_DIR_SIZE * set) != 1) { return nullptr; } return m_vol->dirCache(&pos, options); } //------------------------------------------------------------------------------ bool ExFatFile::close() { bool rtn = sync(); m_attributes = FILE_ATTR_CLOSED; m_flags = 0; return rtn; } //------------------------------------------------------------------------------ bool ExFatFile::contiguousRange(uint32_t* bgnSector, uint32_t* endSector) { if (!isContiguous()) { return false; } if (bgnSector) { *bgnSector = firstSector(); } if (endSector) { *endSector = firstSector() + ((m_validLength - 1) >> m_vol->bytesPerSectorShift()); } return true; } //------------------------------------------------------------------------------ void ExFatFile::fgetpos(fspos_t* pos) const { 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; } //------------------------------------------------------------------------------ uint32_t ExFatFile::firstSector() const { return m_firstCluster ? m_vol->clusterStartSector(m_firstCluster) : 0; } //------------------------------------------------------------------------------ void ExFatFile::fsetpos(const fspos_t* pos) { m_curPosition = pos->position; m_curCluster = pos->cluster; } //------------------------------------------------------------------------------ bool ExFatFile::getAccessDateTime(uint16_t* pdate, uint16_t* ptime) { DirFile_t* df = reinterpret_cast( m_vol->dirCache(&m_dirPos, FsCache::CACHE_FOR_READ)); if (!df) { DBG_FAIL_MACRO; goto fail; } *pdate = getLe16(df->accessDate); *ptime = getLe16(df->accessTime); return true; fail: return false; } //------------------------------------------------------------------------------ bool ExFatFile::getCreateDateTime(uint16_t* pdate, uint16_t* ptime) { DirFile_t* df = reinterpret_cast( m_vol->dirCache(&m_dirPos, FsCache::CACHE_FOR_READ)); if (!df) { DBG_FAIL_MACRO; goto fail; } *pdate = getLe16(df->createDate); *ptime = getLe16(df->createTime); return true; fail: return false; } //------------------------------------------------------------------------------ bool ExFatFile::getModifyDateTime(uint16_t* pdate, uint16_t* ptime) { DirFile_t* df = reinterpret_cast( m_vol->dirCache(&m_dirPos, FsCache::CACHE_FOR_READ)); if (!df) { DBG_FAIL_MACRO; goto fail; } *pdate = getLe16(df->modifyDate); *ptime = getLe16(df->modifyTime); return true; fail: return false; } //------------------------------------------------------------------------------ bool ExFatFile::isBusy() { return m_vol->isBusy(); } //------------------------------------------------------------------------------ bool ExFatFile::open(const char* path, oflag_t oflag) { return open(ExFatVolume::cwv(), path, oflag); } //------------------------------------------------------------------------------ bool ExFatFile::open(ExFatVolume* vol, const char* path, oflag_t oflag) { return vol && open(vol->vwd(), path, oflag); } //------------------------------------------------------------------------------ bool ExFatFile::open(ExFatFile* dirFile, const char* 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 (!openPrivate(dirFile, &fname, O_RDONLY)) { DBG_WARN_MACRO; goto fail; } tmpDir = *this; dirFile = &tmpDir; close(); } return openPrivate(dirFile, &fname, oflag); fail: return false; } //------------------------------------------------------------------------------ bool ExFatFile::open(uint32_t index, oflag_t oflag) { ExFatVolume* vol = ExFatVolume::cwv(); return vol ? open(vol->vwd(), index, oflag) : false; } //------------------------------------------------------------------------------ bool ExFatFile::open(ExFatFile* dirFile, uint32_t index, oflag_t oflag) { if (dirFile->seekSet(FS_DIR_SIZE * index) && openNext(dirFile, oflag)) { if (dirIndex() == index) { return true; } close(); DBG_FAIL_MACRO; } return false; } //------------------------------------------------------------------------------ bool ExFatFile::openCwd() { if (isOpen() || !ExFatVolume::cwv()) { DBG_FAIL_MACRO; goto fail; } *this = *ExFatVolume::cwv()->vwd(); rewind(); return true; fail: return false; } //------------------------------------------------------------------------------ bool ExFatFile::openNext(ExFatFile* dir, oflag_t oflag) { if (isOpen() || !dir->isDir() || (dir->curPosition() & 0X1F)) { DBG_FAIL_MACRO; goto fail; } return openPrivate(dir, nullptr, oflag); fail: return false; } //------------------------------------------------------------------------------ bool ExFatFile::openPrivate(ExFatFile* dir, ExName_t* fname, oflag_t oflag) { int n; uint8_t modeFlags; uint8_t* cache __attribute__((unused)); DirPos_t freePos __attribute__((unused)); DirFile_t* dirFile; DirStream_t* dirStream; DirName_t* dirName; uint8_t buf[FS_DIR_SIZE]; uint8_t freeCount = 0; uint8_t freeNeed = 3; bool inSet = false; // error if already open, no access mode, or no directory. 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 (fname) { freeNeed = 2 + (fname->nameLength + 14) / 15; dir->rewind(); } while (1) { n = dir->read(buf, FS_DIR_SIZE); if (n == 0) { goto create; } if (n != FS_DIR_SIZE) { DBG_FAIL_MACRO; goto fail; } if (!(buf[0] & EXFAT_TYPE_USED)) { // Unused entry. if (freeCount == 0) { freePos.position = dir->curPosition() - FS_DIR_SIZE; freePos.cluster = dir->curCluster(); } if (freeCount < freeNeed) { freeCount++; } if (buf[0] == EXFAT_TYPE_END_DIR) { if (fname) { goto create; } // Likely openNext call. DBG_WARN_MACRO; goto fail; } inSet = false; } else if (!inSet) { if (freeCount < freeNeed) { freeCount = 0; } if (buf[0] != EXFAT_TYPE_FILE) { continue; } inSet = true; memset(this, 0, sizeof(ExFatFile)); dirFile = reinterpret_cast(buf); m_setCount = dirFile->setCount; m_attributes = getLe16(dirFile->attributes) & FS_ATTRIB_COPY; if (!(m_attributes & FS_ATTRIB_DIRECTORY)) { m_attributes |= FILE_ATTR_FILE; } m_vol = dir->volume(); m_dirPos.cluster = dir->curCluster(); m_dirPos.position = dir->curPosition() - FS_DIR_SIZE; m_dirPos.isContiguous = dir->isContiguous(); } else if (buf[0] == EXFAT_TYPE_STREAM) { dirStream = reinterpret_cast(buf); m_flags = modeFlags; if (dirStream->flags & EXFAT_FLAG_CONTIGUOUS) { m_flags |= FILE_FLAG_CONTIGUOUS; } m_validLength = getLe64(dirStream->validLength); m_firstCluster = getLe32(dirStream->firstCluster); m_dataLength = getLe64(dirStream->dataLength); if (!fname) { goto found; } fname->reset(); if (fname->nameLength != dirStream->nameLength || fname->nameHash != getLe16(dirStream->nameHash)) { inSet = false; } } else if (buf[0] == EXFAT_TYPE_NAME) { dirName = reinterpret_cast(buf); if (!cmpName(dirName, fname)) { inSet = false; continue; } if (fname->atEnd()) { goto found; } } else { inSet = false; } } 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() || EXFAT_READ_ONLY) { DBG_FAIL_MACRO; goto fail; } } #if !EXFAT_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; } if (isWritable()) { m_attributes |= FS_ATTRIB_ARCHIVE; } #endif // !EXFAT_READ_ONLY return true; create: #if EXFAT_READ_ONLY DBG_FAIL_MACRO; goto fail; #else // EXFAT_READ_ONLY // don't create unless O_CREAT and write if (!(oflag & O_CREAT) || !(modeFlags & FILE_FLAG_WRITE) || !fname) { DBG_WARN_MACRO; goto fail; } while (freeCount < freeNeed) { n = dir->read(buf, FS_DIR_SIZE); if (n == 0) { uint32_t saveCurCluster = dir->m_curCluster; if (!dir->addDirCluster()) { DBG_FAIL_MACRO; goto fail; } dir->m_curCluster = saveCurCluster; continue; } if (n != FS_DIR_SIZE) { DBG_FAIL_MACRO; goto fail; } if (freeCount == 0) { freePos.position = dir->curPosition() - FS_DIR_SIZE; freePos.cluster = dir->curCluster(); } freeCount++; } freePos.isContiguous = dir->isContiguous(); memset(this, 0, sizeof(ExFatFile)); m_vol = dir->volume(); m_attributes = FILE_ATTR_FILE | FS_ATTRIB_ARCHIVE; m_dirPos = freePos; fname->reset(); for (uint8_t i = 0; i < freeNeed; i++) { cache = dirCache(i, FsCache::CACHE_FOR_WRITE); if (!cache || (cache[0] & 0x80)) { DBG_FAIL_MACRO; goto fail; } memset(cache, 0, FS_DIR_SIZE); if (i == 0) { dirFile = reinterpret_cast(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); setLe16(dirFile->createDate, date); setLe16(dirFile->createTime, time); dirFile->createTimeMs = ms10; } else { setLe16(dirFile->createDate, FS_DEFAULT_DATE); setLe16(dirFile->modifyDate, FS_DEFAULT_DATE); setLe16(dirFile->accessDate, FS_DEFAULT_DATE); if (FS_DEFAULT_TIME) { setLe16(dirFile->createTime, FS_DEFAULT_TIME); setLe16(dirFile->modifyTime, FS_DEFAULT_TIME); setLe16(dirFile->accessTime, FS_DEFAULT_TIME); } } } else if (i == 1) { dirStream = reinterpret_cast(cache); dirStream->type = EXFAT_TYPE_STREAM; dirStream->flags = EXFAT_FLAG_ALWAYS1; m_flags = modeFlags | FILE_FLAG_DIR_DIRTY; dirStream->nameLength = fname->nameLength; setLe16(dirStream->nameHash, fname->nameHash); } else { dirName = reinterpret_cast(cache); dirName->type = EXFAT_TYPE_NAME; for (size_t k = 0; k < 15; k++) { if (fname->atEnd()) { break; } uint16_t u = fname->get16(); setLe16(dirName->unicode + 2 * k, u); } } } return sync(); #endif // EXFAT_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 char* path, ExName_t* fname, const char** ptr) { // Skip leading spaces. while (*path == ' ') { path++; } fname->begin = path; fname->end = path; while (*path && !isDirSeparator(*path)) { uint8_t c = *path++; if (!lfnLegalChar(c)) { DBG_FAIL_MACRO; goto fail; } if (c != '.' && c != ' ') { // Need to trim trailing dots spaces. fname->end = path; } } // Advance to next path component. for (; *path == ' ' || isDirSeparator(*path); path++) { } *ptr = path; return hashName(fname); fail: return false; } //------------------------------------------------------------------------------ int ExFatFile::peek() { uint64_t saveCurPosition = m_curPosition; uint32_t saveCurCluster = m_curCluster; int c = read(); m_curPosition = saveCurPosition; m_curCluster = saveCurCluster; return c; } //------------------------------------------------------------------------------ int ExFatFile::read(void* buf, size_t count) { uint8_t* dst = reinterpret_cast(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->dataCachePrepare(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 reads to current cluster. uint32_t maxNs = m_vol->sectorsPerCluster() - (clusterOffset >> m_vol->bytesPerSectorShift()); if (ns > maxNs) { ns = maxNs; } n = ns << m_vol->bytesPerSectorShift(); if (!m_vol->cacheSafeRead(sector, dst, ns)) { DBG_FAIL_MACRO; goto fail; } #endif // USE_MULTI_SECTOR_IO } else { // read single sector n = m_vol->bytesPerSector(); if (!m_vol->cacheSafeRead(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 char* 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; }