Skip to content

Commit 44245ef

Browse files
committed
feat(usb_host_msc): support multple character encodings
1 parent afb63ab commit 44245ef

File tree

8 files changed

+194
-22
lines changed

8 files changed

+194
-22
lines changed

examples/usb/device/usb_msc_wireless_disk/README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,10 @@ This demo has enabled FATFS OEM multi-code page support. To manually adjust mult
4242

4343
- (Top) → <kbd>Component config</kbd> → <kbd>FAT Filesystem support</kbd> → <kbd>OEM Code Page</kbd>
4444
- This example defaults to <kbd>Dynamic (all code pages supported)</kbd>.
45-
- When set to <kbd>Dynamic (all code pages supported)</kbd> (`FATFS_CODEPAGE_DYNAMIC`), FATFS will support all code pages, but will increase the compiled binary size by ~500kB.
46-
- When set to other code pages, you need to select a code page that matches the character set of the filenames, otherwise it may cause garbled characters, inability to find files, or other hidden issues.
45+
- When set to <kbd>Dynamic (all code pages supported)</kbd> (`FATFS_CODEPAGE_DYNAMIC`), FATFS will support all code pages, but it will increase the size of the compiled output by about 500 kB.
46+
- If you choose another code page, make sure that the selected code page matches the character set used in the filenames; otherwise it may cause garbled text, file not found errors, or other latent issues.
4747
- (Top) → <kbd>Component config</kbd> → <kbd>FAT Filesystem support</kbd> → <kbd>API character encoding</kbd>
4848
- This example defaults to <kbd>API uses UTF-8 encoding</kbd> (`FATFS_API_ENCODING_UTF_8`).
49-
- This option controls the encoding used by FATFS API for reading filenames. Please refer to the description of this configuration item for details.
49+
- This option controls the encoding method used by the filenames read by the FATFS API. For more details, please refer to the configuration help.
5050
- For web development, it is recommended to use UTF-8 encoding.
51+

examples/usb/device/usb_msc_wireless_disk/README_cn.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,9 @@
4242

4343
- (Top) → <kbd>Component config</kbd> → <kbd>FAT Filesystem support</kbd> → <kbd>OEM Code Page</kbd>
4444
- 本例程默认为 <kbd>Dynamic (all code pages supported)</kbd>。
45-
- 设置为 <kbd>Dynamic (all code pages supported)</kbd> (`FATFS_CODEPAGE_DYNAMIC`)时,FATFS 将支持所有代码页,但会使编译产物增大 ~500kB
46-
- 设置为其他代码页时,需要选择与文件名字符集相匹配的代码页,否则会导致乱码、无法找到文件或其他隐性问题。
45+
- 将其设置为 <kbd>Dynamic (all code pages supported)</kbd> (`FATFS_CODEPAGE_DYNAMIC`)时,FATFS 将支持所有代码页,但会使编译产物体积增加约 500 kB
46+
- 若选择其他代码页,请确保所选代码页与文件名字符集匹配,否则会导致乱码、无法找到文件或其他隐性问题。
4747
- (Top) → <kbd>Component config</kbd> → <kbd>FAT Filesystem support</kbd> → <kbd>API character encoding</kbd>
4848
- 本例程默认为 <kbd>API uses UTF-8 encoding</kbd> (`FATFS_API_ENCODING_UTF_8`)。
49-
- 该选项会控制 FATFS API 读出的文件名所用的编码,详见该配置项的说明。
49+
- 该选项控制 FATFS API 读出的文件名所使用的编码方式,详见该配置项的说明。
5050
- 若进行 Web 开发,建议使用 UTF 8 编码。

examples/usb/host/usb_host_msc_example/README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,19 @@ This example demonstrates how to use USB Host MSC functionality and provides a w
1515

1616
Note: exFat requires a paid license.
1717

18+
### FATFS Multilingual Filename Support (Enabled by Default)
19+
20+
This demo has enabled FATFS OEM multi-code page support. To manually adjust multi-language support for filenames, please adjust the following options in menuconfig:
21+
22+
- (Top) → <kbd>Component config</kbd> → <kbd>FAT Filesystem support</kbd> → <kbd>OEM Code Page</kbd>
23+
- This example defaults to <kbd>Dynamic (all code pages supported)</kbd>.
24+
- When set to <kbd>Dynamic (all code pages supported)</kbd> (`FATFS_CODEPAGE_DYNAMIC`), FATFS will support all code pages, but it will increase the size of the compiled output by about 500 kB.
25+
- If you choose another code page, make sure that the selected code page matches the character set used in the filenames; otherwise it may cause garbled text, file not found errors, or other latent issues.
26+
- (Top) → <kbd>Component config</kbd> → <kbd>FAT Filesystem support</kbd> → <kbd>API character encoding</kbd>
27+
- This example defaults to <kbd>API uses UTF-8 encoding</kbd> (`FATFS_API_ENCODING_UTF_8`).
28+
- This option controls the encoding method used by the filenames read by the FATFS API. For more details, please refer to the configuration help.
29+
- For web development, it is recommended to use UTF-8 encoding.
30+
1831
## Web Page Features
1932

2033
- Display files and directories on the MSC device

examples/usb/host/usb_host_msc_example/README_cn.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,19 @@
1515

1616
Note: exFat 是付费许可。
1717

18+
### FATFS 文件名多语言支持(默认启用)
19+
20+
此 Demo 已启用 FATFS 的 OEM 多代码页支持。如需手动调整文件名的多语言支持,请在 menuconfig 中调整下列选项:
21+
22+
- (Top) → <kbd>Component config</kbd> → <kbd>FAT Filesystem support</kbd> → <kbd>OEM Code Page</kbd>
23+
- 本例程默认为 <kbd>Dynamic (all code pages supported)</kbd>。
24+
- 将其设置为 <kbd>Dynamic (all code pages supported)</kbd> (`FATFS_CODEPAGE_DYNAMIC`)时,FATFS 将支持所有代码页,但会使编译产物体积增加约 500 kB。
25+
- 若选择其他代码页,请确保所选代码页与文件名字符集匹配,否则会导致乱码、无法找到文件或其他隐性问题。
26+
- (Top) → <kbd>Component config</kbd> → <kbd>FAT Filesystem support</kbd> → <kbd>API character encoding</kbd>
27+
- 本例程默认为 <kbd>API uses UTF-8 encoding</kbd> (`FATFS_API_ENCODING_UTF_8`)。
28+
- 该选项控制 FATFS API 读出的文件名所使用的编码方式,详见该配置项的说明。
29+
- 若进行 Web 开发,建议使用 UTF 8 编码。
30+
1831
## 网页功能
1932

2033
- 显示 MSC 设备中的文件和目录

examples/usb/host/usb_host_msc_example/components/app_file_web/app_http_server.c

Lines changed: 97 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
#include <sys/param.h>
77
#include <dirent.h>
8+
#include <ctype.h>
89
#include "esp_log.h"
910
#include "esp_vfs.h"
1011
#include "esp_http_server.h"
@@ -14,7 +15,7 @@
1415
#include "sdkconfig.h"
1516

1617
/* Max length a file path can have on storage */
17-
#define FILE_PATH_MAX (ESP_VFS_PATH_MAX + CONFIG_SPIFFS_OBJ_NAME_LEN)
18+
#define FILE_PATH_MAX (ESP_VFS_PATH_MAX + CONFIG_FATFS_MAX_LFN)
1819

1920
/* Scratch buffer size */
2021
#define SCRATCH_BUFSIZE 8192
@@ -46,6 +47,82 @@ struct file_server_data {
4647

4748
static const char *TAG = "file_server";
4849

50+
/* URL decode function to convert percent-encoded characters to their original form */
51+
static size_t url_decode(char *dst, const char *src, size_t dst_size)
52+
{
53+
size_t dst_len = 0;
54+
const char *src_ptr = src;
55+
char *dst_ptr = dst;
56+
57+
while (*src_ptr && dst_len < dst_size - 1) {
58+
if (*src_ptr == '%' && src_ptr[1] && src_ptr[2]) {
59+
/* Check if next two characters are valid hex digits */
60+
if (isxdigit((unsigned char)src_ptr[1]) && isxdigit((unsigned char)src_ptr[2])) {
61+
/* Convert hex digits to character */
62+
uint8_t hex1 = isdigit((unsigned char)src_ptr[1]) ? src_ptr[1] - '0' :
63+
tolower((unsigned char)src_ptr[1]) - 'a' + 10;
64+
uint8_t hex2 = isdigit((unsigned char)src_ptr[2]) ? src_ptr[2] - '0' :
65+
tolower((unsigned char)src_ptr[2]) - 'a' + 10;
66+
67+
*dst_ptr++ = (char)((hex1 << 4) | hex2);
68+
src_ptr += 3;
69+
dst_len++;
70+
} else {
71+
/* Invalid hex sequence, copy as-is */
72+
*dst_ptr++ = *src_ptr++;
73+
dst_len++;
74+
}
75+
} else if (*src_ptr == '+') {
76+
/* Convert '+' to space (mainly for query parameters, but harmless in paths) */
77+
*dst_ptr++ = ' ';
78+
src_ptr++;
79+
dst_len++;
80+
} else {
81+
/* Copy character as-is */
82+
*dst_ptr++ = *src_ptr++;
83+
dst_len++;
84+
}
85+
}
86+
87+
*dst_ptr = '\0';
88+
return dst_len;
89+
}
90+
91+
/* URL encode function to convert characters to percent-encoded form for safe use in URLs */
92+
static size_t url_encode(char *dst, const char *src, size_t dst_size)
93+
{
94+
size_t dst_len = 0;
95+
const char *src_ptr = src;
96+
char *dst_ptr = dst;
97+
98+
while (*src_ptr && dst_len < dst_size - 1) {
99+
char c = *src_ptr;
100+
101+
/* Check if character needs encoding */
102+
if (isalnum((unsigned char)c) || c == '-' || c == '_' || c == '.' || c == '~' || c == '/') {
103+
/* Safe character, copy as-is */
104+
*dst_ptr++ = c;
105+
dst_len++;
106+
} else {
107+
/* Unsafe character, encode as %XX */
108+
if (dst_len + 3 >= dst_size) {
109+
/* Not enough space for encoding */
110+
break;
111+
}
112+
uint8_t high = (c >> 4) & 0x0F;
113+
uint8_t low = c & 0x0F;
114+
*dst_ptr++ = '%';
115+
*dst_ptr++ = (high < 10) ? ('0' + high) : ('A' + high - 10);
116+
*dst_ptr++ = (low < 10) ? ('0' + low) : ('A' + low - 10);
117+
dst_len += 3;
118+
}
119+
src_ptr++;
120+
}
121+
122+
*dst_ptr = '\0';
123+
return dst_len;
124+
}
125+
49126
/* Handler to redirect incoming GET request for /index.html to /
50127
* This can be overridden by uploading file with same name */
51128
static esp_err_t index_html_get_handler(httpd_req_t *req)
@@ -162,12 +239,16 @@ static esp_err_t http_resp_dir_html(httpd_req_t *req, const char *dirpath)
162239
sprintf(entrysize, "%ld", entry_stat.st_size);
163240
ESP_LOGI(TAG, "Found %s : %s (%s bytes)", entrytype, entry->d_name, entrysize);
164241

242+
/* URL encode the filename for safe use in HTML href attributes */
243+
char encoded_filename[CONFIG_FATFS_MAX_LFN * 3 + 1];
244+
url_encode(encoded_filename, entry->d_name, sizeof(encoded_filename));
245+
165246
/* Send chunk of HTML file containing table entries with file name and size */
166247
httpd_resp_sendstr_chunk(req, "<tr><td class=\"");
167248
httpd_resp_sendstr_chunk(req, entrytype);
168249
httpd_resp_sendstr_chunk(req, "\"><a href=\"");
169250
httpd_resp_sendstr_chunk(req, req->uri);
170-
httpd_resp_sendstr_chunk(req, entry->d_name);
251+
httpd_resp_sendstr_chunk(req, encoded_filename);
171252
if (entry->d_type == DT_DIR) {
172253
httpd_resp_sendstr_chunk(req, "/");
173254
}
@@ -178,7 +259,7 @@ static esp_err_t http_resp_dir_html(httpd_req_t *req, const char *dirpath)
178259
httpd_resp_sendstr_chunk(req, "</td><td>");
179260
httpd_resp_sendstr_chunk(req, "<button class=\"deleteButton\" filepath=\"");
180261
httpd_resp_sendstr_chunk(req, req->uri);
181-
httpd_resp_sendstr_chunk(req, entry->d_name);
262+
httpd_resp_sendstr_chunk(req, encoded_filename);
182263
httpd_resp_sendstr_chunk(req, "\">Delete</button>");
183264
httpd_resp_sendstr_chunk(req, "</td></tr>\n");
184265
}
@@ -217,7 +298,7 @@ static esp_err_t set_content_type_from_file(httpd_req_t *req, const char *filena
217298
}
218299

219300
/* Copies the full path into destination buffer and returns
220-
* pointer to path (skipping the preceding base path) */
301+
* pointer to path (skipping the preceding base path) with URL decoding */
221302
static const char *get_path_from_uri(char *dest, const char *base_path, const char *uri, size_t destsize)
222303
{
223304
const size_t base_pathlen = strlen(base_path);
@@ -232,14 +313,23 @@ static const char *get_path_from_uri(char *dest, const char *base_path, const ch
232313
pathlen = MIN(pathlen, hash - uri);
233314
}
234315

235-
if (base_pathlen + pathlen + 1 > destsize) {
316+
/* Create temporary buffer for URL decoding */
317+
char *temp_uri = alloca(pathlen + 1);
318+
strlcpy(temp_uri, uri, pathlen + 1);
319+
320+
/* URL decode the path portion */
321+
char *decoded_path = alloca(pathlen + 1);
322+
size_t decoded_len = url_decode(decoded_path, temp_uri, pathlen + 1);
323+
324+
/* Check if full path (base + decoded path) fits in destination buffer */
325+
if (base_pathlen + decoded_len + 1 > destsize) {
236326
/* Full path string won't fit into destination buffer */
237327
return NULL;
238328
}
239329

240-
/* Construct full path (base + path) */
330+
/* Construct full path (base + decoded path) */
241331
strcpy(dest, base_path);
242-
strlcpy(dest + base_pathlen, uri, pathlen + 1);
332+
strcpy(dest + base_pathlen, decoded_path);
243333

244334
/* Return pointer to path, skipping the base */
245335
return dest + base_pathlen;

examples/usb/host/usb_host_msc_example/components/app_file_web/spiffs/file_list_1.html

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@
1010
alert('Failed to delete the file.');
1111
return;
1212
}
13+
if (!enableResetButton) {
14+
alert("File deleted!");
15+
location.reload();
16+
return;
17+
}
1318
const cookies = document.cookie.split('; ').reduce((acc, cookie) => {
1419
const [name, value] = cookie.split('=');
1520
acc[name] = value;

examples/usb/host/usb_host_msc_example/components/app_file_web/spiffs/upload.html

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -124,31 +124,53 @@
124124
validateInputs();
125125
}
126126

127+
// Calculate UTF-8 byte length of a string
128+
function getByteLength(str) {
129+
return new TextEncoder().encode(str).length;
130+
}
131+
132+
// Check for invalid filesystem characters
133+
function hasInvalidPathChars(path) {
134+
// Invalid characters for paths: control chars, <, >, :, ", |, ?, *, \
135+
return /[\x00-\x1f<>:"|?*\\]/.test(path);
136+
}
137+
138+
function hasInvalidFileNameChars(fileName) {
139+
// Invalid characters for filenames: control chars, <, >, :, ", |, ?, *, \, /
140+
return /[\x00-\x1f<>:"|?*\\/]/.test(fileName);
141+
}
142+
127143
function validateInputs() {
128144
const uploadPath = uploadPathInput.value;
129145
const fileName = fileNameInput.value;
130-
146+
// Validate upload path
131147
if (
132148
!uploadPath.startsWith("/") ||
133149
!uploadPath.endsWith("/") ||
134-
/[^a-zA-Z0-9\.\/\_\-]/.test(uploadPath) ||
150+
hasInvalidPathChars(uploadPath) ||
135151
uploadPath.startsWith("/upload/") ||
136152
uploadPath.startsWith("/api/") ||
137-
/\/\//.test(uploadPath)
153+
/\/\//.test(uploadPath) ||
154+
getByteLength(uploadPath) > 255
138155
) {
139-
pathError.textContent = "Invalid upload path.";
156+
pathError.textContent = "Invalid upload path. Path must start and end with /, contain no invalid characters, not exceed 255 bytes, and not start with /upload/ or /api/.";
140157
} else {
141158
pathError.textContent = "";
142159
}
143-
160+
// Validate file name
144161
if (
145-
/[^a-zA-Z0-9\.\_\-]/.test(fileName) ||
146-
fileName.length > 32 ||
162+
hasInvalidFileNameChars(fileName) ||
163+
getByteLength(fileName) > 64 ||
147164
fileName === "upload.html" ||
148165
fileName === "api" ||
149-
fileName === "settings.html"
166+
fileName === "settings.html" ||
167+
fileName.trim() !== fileName ||
168+
fileName.endsWith(".") ||
169+
fileName === "" ||
170+
fileName === "." ||
171+
fileName === ".."
150172
) {
151-
nameError.textContent = "Invalid file name.";
173+
nameError.textContent = "Invalid file name. Must not contain invalid characters (< > : \" | ? * \\ /), not exceed 64 bytes, and not be a reserved name.";
152174
} else {
153175
nameError.textContent = "";
154176
}

examples/usb/host/usb_host_msc_example/sdkconfig.defaults

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,36 @@
22
# Espressif IoT Development Framework (ESP-IDF) 5.5.0 Project Minimal Configuration
33
#
44
CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240=y
5+
CONFIG_WL_SECTOR_SIZE=4096
6+
7+
#
8+
# FAT Filesystem support
9+
#
10+
CONFIG_FATFS_CODEPAGE_DYNAMIC=y
11+
CONFIG_FATFS_CODEPAGE=0
512
CONFIG_FATFS_LFN_HEAP=y
13+
CONFIG_FATFS_MAX_LFN=255
14+
CONFIG_FATFS_API_ENCODING_UTF_8=y
15+
CONFIG_FATFS_FS_LOCK=0
16+
CONFIG_FATFS_TIMEOUT_MS=10000
17+
CONFIG_FATFS_PER_FILE_CACHE=y
18+
# End of FAT Filesystem support
19+
620
CONFIG_FATFS_USE_LABEL=y
721
CONFIG_FREERTOS_HZ=1000
822
CONFIG_USB_HOST_CONTROL_TRANSFER_MAX_SIZE=1024
923
CONFIG_HTTPD_MAX_REQ_HDR_LEN=1024
24+
25+
#
26+
# Partition Table
27+
#
28+
# CONFIG_PARTITION_TABLE_SINGLE_APP is not set
29+
CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y
30+
# CONFIG_PARTITION_TABLE_TWO_OTA is not set
31+
# CONFIG_PARTITION_TABLE_TWO_OTA_LARGE is not set
32+
# CONFIG_PARTITION_TABLE_CUSTOM is not set
33+
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv"
34+
CONFIG_PARTITION_TABLE_FILENAME="partitions_singleapp_large.csv"
35+
CONFIG_PARTITION_TABLE_OFFSET=0x8000
36+
CONFIG_PARTITION_TABLE_MD5=y
37+
# end of Partition Table

0 commit comments

Comments
 (0)