Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix M23 select long filename for print #25540

Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 47 additions & 32 deletions Marlin/src/sd/SdBaseFile.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -703,14 +703,18 @@ bool SdBaseFile::open(SdBaseFile *dirFile, const uint8_t dname[11]
// Get VFat dir entry
pvFat = (vfat_t *) p;
// Get checksum from the last entry of the sequence
if (pvFat->sequenceNumber & 0x40) lfnChecksum = pvFat->checksum;
if (pvFat->sequenceNumber & 0x40) {
lfnChecksum = pvFat->checksum;
memset(lfnName, '\0', sizeof(lfnName));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to clear the whole buffer here to ensure a null terminator later, or can we get away with just lnfName[0] = '\0'?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I guess it wouldn't work: lfnName is filled from the end of the actual LFN found in VFAT sequences (which appear in reverse order; see method getLFNName), which means just using lfnName[0] = '\0' wouldn't suffice.
However, the code below

if (!strncasecmp((char*)dlname, (char*)lfnName, lfnNameLength)) lfnFileFound = true;

compares these chararrays up until lfnNameLength, which is initialized as

lfnNameLength = useLFN ? strlen((char*)dlname) : 0,

above, which means we should compare these strings up until requested part length. Which means, yes, we probably don't even need this line at all and it should work OK for comparison.

Copy link
Contributor Author

@eduard-sukharev eduard-sukharev Mar 24, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although, on second thought, If we remove the memset, then we're probably prone to error cases.
Let's assume we have files:

/my_cool_model.gcode
/important.log
/important.log.gcode

then, in certain conditions, attempting to open /important.log.gcode will actually open /important.log file.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally, when copying strings into buffers we should append a nul after writing the other bytes. This avoids the need to clear the whole buffer before the copy. The memset might use some extra electrons, but it's minor thing in practical terms. Only the code size ultimately matters, not the performance, since this is not a bottleneck.

}
// Get LFN sequence number
lfnSequenceNumber = pvFat->sequenceNumber & 0x1F;
if WITHIN(lfnSequenceNumber, 1, reqEntriesNum) {
// Check checksum for all other entries with the starting checksum fetched before
if (lfnChecksum == pvFat->checksum) {
// Set chunk of LFN from VFAT entry into lfnName
getLFNName(pvFat, (char *)lfnName, lfnSequenceNumber);
TERN_(UTF_FILENAME_SUPPORT, convertUtf16ToUtf8((char *)lfnName));
// LFN found?
if (!strncasecmp((char*)dlname, (char*)lfnName, lfnNameLength)) lfnFileFound = true;
}
Expand Down Expand Up @@ -1424,7 +1428,7 @@ int16_t SdBaseFile::read(void *buf, uint16_t nbyte) {
* readDir() called before a directory has been opened, this is not
* a directory file or an I/O error occurred.
*/
int8_t SdBaseFile::readDir(dir_t *dir, char *longFilename) {
int8_t SdBaseFile::readDir(dir_t *dir, char * const longFilename) {
int16_t n;
// if not a directory file or miss-positioned return an error
if (!isDir() || (0x1F & curPosition_)) return -1;
Expand Down Expand Up @@ -1506,44 +1510,55 @@ int8_t SdBaseFile::readDir(dir_t *dir, char *longFilename) {
// Post-process normal file or subdirectory longname, if any
if (DIR_IS_FILE_OR_SUBDIR(dir)) {
#if ENABLED(UTF_FILENAME_SUPPORT)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although this is the original behavior, I guess UCS-2 to UTF-8 conversion should be done always, since VFat always stores long names as UCS-2 (UTF-16) in LFN sequences, while we always handle 8bit string names.

#if LONG_FILENAME_CHARSIZE > 2
// Add warning for developers for unsupported 3-byte cases.
// (Converting 2-byte codepoints to 3-byte in-place would break the rest of filename.)
#error "Currently filename re-encoding is done in-place. It may break the remaining chars to use 3-byte codepoints."
#endif

// Is there a long filename to decode?
if (longFilename) {
// Reset n to the start of the long name
n = 0;
for (uint16_t idx = 0; idx < (LONG_FILENAME_LENGTH); idx += 2) { // idx is fixed since FAT LFN always contains UTF-16LE encoding
const uint16_t utf16_ch = longFilename[idx] | (longFilename[idx + 1] << 8);
if (0xD800 == (utf16_ch & 0xF800)) // Surrogate pair - encode as '_'
longFilename[n++] = '_';
else if (0 == (utf16_ch & 0xFF80)) // Encode as 1-byte UTF-8 char
longFilename[n++] = utf16_ch & 0x007F;
else if (0 == (utf16_ch & 0xF800)) { // Encode as 2-byte UTF-8 char
longFilename[n++] = 0xC0 | ((utf16_ch >> 6) & 0x1F);
longFilename[n++] = 0x80 | ( utf16_ch & 0x3F);
}
else {
#if LONG_FILENAME_CHARSIZE > 2 // Encode as 3-byte UTF-8 char
longFilename[n++] = 0xE0 | ((utf16_ch >> 12) & 0x0F);
longFilename[n++] = 0xC0 | ((utf16_ch >> 6) & 0x3F);
longFilename[n++] = 0xC0 | ( utf16_ch & 0x3F);
#else // Encode as '_'
longFilename[n++] = '_';
#endif
}
if (0 == utf16_ch) break; // End of filename
} // idx
} // longFilename
n = convertUtf16ToUtf8(longFilename);
}
#endif
return n;
} // DIR_IS_FILE_OR_SUBDIR
}
}

#if ENABLED(UTF_FILENAME_SUPPORT)

uint8_t SdBaseFile::convertUtf16ToUtf8(char * const longFilename) {
#if LONG_FILENAME_CHARSIZE > 2
// Add warning for developers for unsupported 3-byte cases.
// (Converting 2-byte codepoints to 3-byte in-place would break the rest of filename.)
#error "Currently filename re-encoding is done in-place. It may break the remaining chars to use 3-byte codepoints."
#endif

int16_t n;
// Reset n to the start of the long name
n = 0;
for (uint16_t idx = 0; idx < (LONG_FILENAME_LENGTH); idx += 2) { // idx is fixed since FAT LFN always contains UTF-16LE encoding
const uint16_t utf16_ch = longFilename[idx] | (longFilename[idx + 1] << 8);
if (0xD800 == (utf16_ch & 0xF800)) // Surrogate pair - encode as '_'
longFilename[n++] = '_';
else if (0 == (utf16_ch & 0xFF80)) // Encode as 1-byte UTF-8 char
longFilename[n++] = utf16_ch & 0x007F;
else if (0 == (utf16_ch & 0xF800)) { // Encode as 2-byte UTF-8 char
longFilename[n++] = 0xC0 | ((utf16_ch >> 6) & 0x1F);
longFilename[n++] = 0x80 | ( utf16_ch & 0x3F);
}
else {
#if LONG_FILENAME_CHARSIZE > 2 // Encode as 3-byte UTF-8 char
longFilename[n++] = 0xE0 | ((utf16_ch >> 12) & 0x0F);
longFilename[n++] = 0xC0 | ((utf16_ch >> 6) & 0x3F);
longFilename[n++] = 0xC0 | ( utf16_ch & 0x3F);
#else // Encode as '_'
longFilename[n++] = '_';
#endif
}
if (0 == utf16_ch) break; // End of filename
} // idx

return n;
}

#endif // UTF_FILENAME_SUPPORT

// Read next directory entry into the cache
// Assumes file is correctly positioned
dir_t* SdBaseFile::readDirCache() {
Expand Down
10 changes: 6 additions & 4 deletions Marlin/src/sd/SdBaseFile.h
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,7 @@ class SdBaseFile {
bool printName();
int16_t read();
int16_t read(void *buf, uint16_t nbyte);
int8_t readDir(dir_t *dir, char *longFilename);
int8_t readDir(dir_t *dir, char * const longFilename);
static bool remove(SdBaseFile *dirFile, const char *path);
bool remove();

Expand Down Expand Up @@ -392,14 +392,16 @@ class SdBaseFile {
bool openCachedEntry(uint8_t cacheIndex, uint8_t oflags);
dir_t* readDirCache();

#if ENABLED(UTF_FILENAME_SUPPORT)
uint8_t convertUtf16ToUtf8(char * const longFilename);
#endif

// Long Filename create/write support
#if ENABLED(LONG_FILENAME_WRITE_SUPPORT)
static bool isDirLFN(const dir_t* dir);
static bool isDirNameLFN(const char *dirname);
static bool parsePath(const char *str, uint8_t *name, uint8_t *lname, const char **ptr);
/**
* Return the number of entries needed in the FAT for this LFN
*/
// Return the number of entries needed in the FAT for this LFN
static inline uint8_t getLFNEntriesNum(const char *lname) { return (strlen(lname) + 12) / 13; }
static void getLFNName(vfat_t *vFatDir, char *lname, uint8_t startOffset);
static void setLFNName(vfat_t *vFatDir, char *lname, uint8_t lfnSequenceNumber);
Expand Down