// SPDX-License-Identifier: LGPL-2.0-only // SPDX-FileCopyrightText: 2023 Oleksij Rempel #include #include #include #include #include #include "isobusfs_srv.h" #include "isobusfs_cmn_dh.h" void isobusfs_srv_set_default_current_dir(struct isobusfs_srv_priv *priv, struct isobusfs_srv_client *client) { snprintf(client->current_dir, ISOBUSFS_SRV_MAX_PATH_LEN, "\\\\%s", priv->default_volume); } static const char *isobusfs_srv_get_volume_end(const char *path, size_t path_size) { const char *vol_end = NULL; size_t i; if (!path || !path_size) return NULL; if (!(path[0] == '\\' && path[1] == '\\' && path[2] != '\0')) return NULL; for (i = 2; i < path_size; i++) { if (path[i] == '\\' || path[i] == '\0') { vol_end = &path[i]; break; } } if (!vol_end) vol_end = &path[i]; return vol_end; } int isobusfs_path_to_linux_path(struct isobusfs_srv_priv *priv, const char *isobusfs_path, size_t isobusfs_path_size, char *linux_path, size_t linux_path_size) { struct isobusfs_srv_volume *volume = NULL; size_t isobusfs_path_pos = 0; const char *vol_end; char *ptr; int i; if (!priv || !isobusfs_path || !linux_path || !linux_path_size || !isobusfs_path_size) { pr_err("%s: invalid argument\n", __func__); return -EINVAL; } vol_end = isobusfs_srv_get_volume_end(isobusfs_path, isobusfs_path_size); if (!vol_end) { pr_err("%s: invalid path %s. Can't find end of volume string\n", __func__, isobusfs_path); return -EINVAL; } /* Search for the volume in the priv->volumes array */ for (i = 0; i < priv->volume_count; i++) { size_t volume_name_len = vol_end - (isobusfs_path + 2); if (volume_name_len == strlen(priv->volumes[i].name) && memcmp(priv->volumes[i].name, isobusfs_path + 2, volume_name_len) == 0) { volume = &priv->volumes[i]; break; } } if (!volume) { pr_err("%s: invalid path %s. Can't find volume\n", __func__, isobusfs_path); return -ENODEV; } /* Copy the volume's Linux path to the output buffer */ strncpy(linux_path, volume->path, linux_path_size - 1); linux_path[linux_path_size - 1] = '\0'; isobusfs_path_pos = vol_end - isobusfs_path; /* Add a forward slash if path ends after volume name */ if (*vol_end == '\0' || isobusfs_path_pos == isobusfs_path_size - 1) strncat(linux_path, "/", linux_path_size - strlen(linux_path) - 1); if (isobusfs_path_pos + 3 < isobusfs_path_size && strncmp(vol_end, "\\~\\", 3) == 0) { strncat(linux_path, "/", linux_path_size - strlen(linux_path) - 1); /* convert tilde to manufacturer-specific directory */ strncat(linux_path, priv->mfs_dir, linux_path_size - strlen(linux_path) - 1); vol_end += 2; } /* Replace backslashes with forward slashes for the rest of the path */ ptr = linux_path + strlen(linux_path); while (vol_end < isobusfs_path + isobusfs_path_size && *vol_end) { if (*vol_end == '\\') *ptr = '/'; else *ptr = *vol_end; ptr++; vol_end++; if (ptr - linux_path >= (long int)linux_path_size) { /* Ensure null termination */ linux_path[linux_path_size - 1] = '\0'; break; } } return 0; } int isobusfs_check_current_dir_access(struct isobusfs_srv_priv *priv, const char *path, size_t path_size) { char linux_path[ISOBUSFS_SRV_MAX_PATH_LEN]; int ret; ret = isobusfs_path_to_linux_path(priv, path, path_size, linux_path, sizeof(linux_path)); if (ret < 0) return ret; pr_debug("convert ISOBUS FS path to linux path: %.*s -> %s", path_size, path, linux_path); ret = isobusfs_cmn_dh_validate_dir_path(linux_path, false); if (ret < 0) return ret; return 0; } /* current directory response function */ static int isobusfs_srv_dh_current_dir_res(struct isobusfs_srv_priv *priv, struct isobusfs_msg *msg) { struct isobusfs_dh_get_cd_req *req = (struct isobusfs_dh_get_cd_req *)msg->buf; uint8_t error_code = ISOBUSFS_ERR_SUCCESS; struct isobusfs_dh_get_cd_res *res; struct isobusfs_srv_client *client; size_t str_len, buf_size; size_t fixed_res_size; size_t padding_size = 0; int ret; client = isobusfs_srv_get_client_by_msg(priv, msg); if (!client) { pr_warn("client not found"); return -ENOENT; } if (client->current_dir[0] == '\0') isobusfs_srv_set_default_current_dir(priv, client); ret = isobusfs_check_current_dir_access(priv, client->current_dir, sizeof(client->current_dir)); if (ret < 0) { switch (ret) { case -ENOENT: error_code = ISOBUSFS_ERR_FILE_ORPATH_NOT_FOUND; case -ENOMEDIUM: error_code = ISOBUSFS_ERR_VOLUME_NOT_INITIALIZED; case -ENOMEM: error_code = ISOBUSFS_ERR_OUT_OF_MEM; default: error_code = ISOBUSFS_ERR_OTHER; } } fixed_res_size = sizeof(*res); str_len = strlen(client->current_dir) + 1; buf_size = fixed_res_size + str_len; if (buf_size > ISOBUSFS_MAX_TRANSFER_LENGH) { pr_warn("current directory response too long"); /* Calculate the maximum allowed string length based on the * buffer size */ str_len = ISOBUSFS_MAX_TRANSFER_LENGH - fixed_res_size; /* Update the buffer size accordingly */ buf_size = fixed_res_size + str_len; error_code = ISOBUSFS_ERR_OUT_OF_MEM; } else if (buf_size < ISOBUSFS_MIN_TRANSFER_LENGH) { /* Update the buffer size accordingly */ padding_size = ISOBUSFS_MIN_TRANSFER_LENGH - buf_size; buf_size = ISOBUSFS_MIN_TRANSFER_LENGH; } res = malloc(buf_size); if (!res) { pr_err("failed to allocate memory for current directory response"); return -ENOMEM; } res->fs_function = isobusfs_cg_function_to_buf(ISOBUSFS_CG_DIRECTORY_HANDLING, ISOBUSFS_DH_F_GET_CURRENT_DIR_RES); res->tan = req->tan; res->error_code = error_code; /* TODO: implement total_space and free_space */ res->total_space = htole16(0); res->free_space = htole16(0); res->name_len = htole16(str_len); memcpy(res->name, client->current_dir, str_len); if (padding_size) { /* Fill the rest of the res structure with 0xff */ memset(((uint8_t *)res) + buf_size - padding_size, 0xff, padding_size); } /* send to socket */ ret = isobusfs_srv_sendto(priv, msg, res, buf_size); if (ret < 0) { pr_warn("can't send current directory response"); goto free_res; } pr_debug("> tx: current directory response: %s, total space: %i, free space: %i", client->current_dir, le16toh(res->total_space), le16toh(res->free_space)); free_res: free(res); return ret; } /** * isobusfs_is_forbidden_char() - check if the given character is forbidden * @ch: character to check * * Return: true if the character is forbidden, false otherwise * * The function checks if the given character is forbidden in the ISOBUS FS * as defined in ISO 11783-13:2021, section A.2.2.1 Names: * To avoid incompatibility between different operating systems, the client * shall not create folder/files with names, which only differs in case, and * names shall not end with a '.' or include ‘<’, ‘>’, ‘|’ (the latter three * may cause issues on FAT32). * .... * LongNameChar ::= any single character defined by Unicode/ISO/IEC 10646, * except 0x00 to 0x1f, 0x7f to 0x9f, ‘\’, ‘*’, ‘?’, ‘/’. */ static bool isobusfs_is_forbidden_char(wchar_t ch) { if (ch >= 0x00 && ch <= 0x1f) return true; if (ch >= 0x7f && ch <= 0x9f) return true; if (ch == L'*' || ch == L'?' || ch == L'/' || ch == L'<' || ch == L'>' || ch == L'|') return true; return false; } static int isobusfs_validate_path_chars(const char *path, size_t size) { for (size_t i = 0; i < size; ++i) { wchar_t ch = path[i]; if (isobusfs_is_forbidden_char(ch)) return -EINVAL; } return 0; } static int isobusfs_handle_path_prefix(const char *current_dir, size_t current_dir_len, const char *rel_path, size_t rel_path_size, size_t *rel_path_pos, char *abs_path, size_t abs_path_size, size_t *abs_path_pos) { if (strncmp(rel_path, "~\\", 2) == 0) { size_t vol_len; const char *vol_end; vol_end = isobusfs_srv_get_volume_end(current_dir, current_dir_len); if (!vol_end) return -EINVAL; vol_len = vol_end - current_dir; strncpy(abs_path, current_dir, vol_len); abs_path[vol_len] = '\\'; *abs_path_pos = vol_len + 1; } else if (strncmp(rel_path, "\\\\", 2) == 0) { /* Too many back slashes, drop it. */ if (rel_path[2] == '\\') return -EINVAL; strncpy(abs_path, rel_path, 2); *abs_path_pos = 2; *rel_path_pos = 2; } else { strncpy(abs_path, current_dir, abs_path_size); *abs_path_pos = current_dir_len; if (abs_path[*abs_path_pos - 1] != '\\') { if (*abs_path_pos < abs_path_size - 1) { abs_path[*abs_path_pos] = '\\'; *abs_path_pos += 1; } else { return -ENOMEM; } } if (rel_path[*rel_path_pos] == '\\') *rel_path_pos += 1; } return 0; } /** * is_valid_path_char - Check if the current character is valid in the path * @rel_path: The relative path being processed * @rel_path_size: The size of the relative path * @rel_path_pos: Pointer to the current position in the relative path * * Checks if the current character at the position in the relative path * is not the end of the string, not a null character, and not a backslash. * * Return: True if the current character is valid, False otherwise. */ static bool is_valid_path_char(const char *rel_path, size_t rel_path_size, const size_t *rel_path_pos) { return *rel_path_pos < rel_path_size && rel_path[*rel_path_pos] != '\0' && rel_path[*rel_path_pos] != '\\'; } /** * Checks if the specified number of positions ahead in the relative path * are either the end of the buffer or a backslash. * * @param rel_path The relative path being processed. * @param rel_path_size The size of the relative path. * @param rel_path_pos The current position in the relative path. * @param look_ahead The number of positions ahead to check. * @return True if the specified positions ahead are the end or a backslash, * False otherwise. */ static bool is_end_or_backslash(const char *rel_path, size_t rel_path_size, const size_t *rel_path_pos, size_t look_ahead) { if (*rel_path_pos + look_ahead >= rel_path_size) return true; /* End of buffer */ return rel_path[*rel_path_pos + look_ahead] == '\\'; } /** * is_path_separator - Check if the current character is a path separator * @rel_path: The relative path being processed * @rel_path_size: The size of the relative path * @rel_path_pos: Pointer to the current position in the relative path * * Checks if the current character at the position in the relative path * is a backslash and the position is within the string size. * * Return: True if the current character is a backslash, False otherwise. */ static bool is_path_separator(const char *rel_path, size_t rel_path_size, const size_t *rel_path_pos) { return *rel_path_pos < rel_path_size && rel_path[*rel_path_pos] == '\\'; } static bool isobusfs_is_dot_directive(const char *rel_path, size_t rel_path_size, const size_t *rel_path_pos) { if (rel_path[*rel_path_pos] == '.') { /* Check for '.' followed by a backslash or at the end of the * string */ if (is_end_or_backslash(rel_path, rel_path_size, rel_path_pos, 1) || rel_path[*rel_path_pos + 1] == '\0') { return true; } /* Check for '..' followed by a backslash or at the end of the * string */ if (rel_path[*rel_path_pos + 1] == '.') { if (is_end_or_backslash(rel_path, rel_path_size, rel_path_pos, 2) || rel_path[*rel_path_pos + 2] == '\0') { return true; } } } return false; } /** * isobusfs_handle_single_dot - Processes a single dot directive in a relative * path * @rel_path: The relative path being processed * @rel_path_size: The size of the relative path * @rel_path_pos: Pointer to the current position in the relative path * * This function checks if the current segment in the relative path is a single * dot ('.'). A single dot represents the current directory. If the next * character after the dot is either a backslash or the end of the string, * the function advances the path position appropriately. The function returns * true if it processes a single dot, indicating that the current directory * directive was found and handled. * * Return: True if a single dot directive is detected, False otherwise. */ static bool isobusfs_handle_single_dot(const char *rel_path, size_t rel_path_size, size_t *rel_path_pos) { bool is_dot = false; if (is_end_or_backslash(rel_path, rel_path_size, rel_path_pos, 1)) { *rel_path_pos += 2; is_dot = true; } else if (rel_path[*rel_path_pos + 1] == '\0') { *rel_path_pos += 1; is_dot = true; } return is_dot; } /** * isobusfs_handle_double_dots - Processes a double dot directive in a relative * path * * @rel_path: The relative path being processed * @rel_path_size: The size of the relative path * @rel_path_pos: Pointer to the current position in the relative path * @abs_path: Buffer to store the absolute path being constructed * @abs_path_pos: Pointer to the current position in the absolute path buffer * * This function processes the double dot directive ('..') in a relative path. * The double dot represents the parent directory. If the double dot directive * is followed by a backslash or is at the end of the string, the function * advances the path position accordingly. Additionally, it adjusts the * absolute path position to move up one directory in the path hierarchy. The * function ensures that it does not go beyond the root of the absolute path * while moving up the directory hierarchy. */ static void isobusfs_handle_double_dots(const char *rel_path, size_t rel_path_size, size_t *rel_path_pos, char *abs_path, size_t *abs_path_pos) { /* Move the relative path position forward after handling '..' */ if (is_end_or_backslash(rel_path, rel_path_size, rel_path_pos, 2)) *rel_path_pos += 3; else if (rel_path[*rel_path_pos + 2] == '\0') *rel_path_pos += 2; /* Move the absolute path position backward to simulate moving up a * directory */ if (*abs_path_pos > 2 && abs_path[*abs_path_pos - 1] == '\\') *abs_path_pos -= 1; while (*abs_path_pos > 2 && abs_path[*abs_path_pos - 1] != '\\') *abs_path_pos -= 1; } /** * isobusfs_handle_dot_directive - Processes '.' and '..' directives in a path * @rel_path: The relative path being processed * @rel_path_size: The size of the relative path * @rel_path_pos: Pointer to the current position in the relative path * @abs_path: Buffer to store the absolute path being constructed * @abs_path_pos: Pointer to the current position in the absolute path buffer * * This function processes the dot directives found in a relative path. It * handles both single dot ('.') and double dot ('..') directives. A single dot * represents the current directory, while a double dot represents moving up to * the parent directory. */ static void isobusfs_handle_dot_directive(const char *rel_path, size_t rel_path_size, size_t *rel_path_pos, char *abs_path, size_t *abs_path_pos) { if (rel_path[*rel_path_pos] == '.') { bool is_dot = isobusfs_handle_single_dot(rel_path, rel_path_size, rel_path_pos); if (!is_dot && rel_path[*rel_path_pos + 1] == '.') { isobusfs_handle_double_dots(rel_path, rel_path_size, rel_path_pos, abs_path, abs_path_pos); } } /* Skip additional backslashes after '.' or '..' */ while (is_path_separator(rel_path, rel_path_size, rel_path_pos)) *rel_path_pos += 1; } /** * isobusfs_process_path_segment - Processes normal path segments * @rel_path: The relative path being processed * @rel_path_size: The size of the relative path * @rel_path_pos: Pointer to the current position in the relative path * @abs_path: The buffer to store the absolute path * @abs_path_size: The size of the absolute path buffer * @abs_path_pos: Pointer to the position in the absolute path buffer * * This function processes normal segments of a relative path, copying them * into the absolute path buffer. It handles each character until it encounters * a path separator or reaches the end of the relative path. If a path separator * is found, it adds a single backslash to the absolute path. The function * ensures that the buffer limits are respected to prevent buffer overflows. * * Return: 0 on successful processing of the segment, -ENOMEM if the absolute * path buffer runs out of space. */ static int isobusfs_process_path_segment(const char *rel_path, size_t rel_path_size, size_t *rel_path_pos, char *abs_path, size_t abs_path_size, size_t *abs_path_pos) { /* Process the current character from the relative path */ abs_path[*abs_path_pos] = rel_path[*rel_path_pos]; *abs_path_pos += 1; *rel_path_pos += 1; /* Continue processing until a path separator or end of the string is * reached */ while (is_valid_path_char(rel_path, rel_path_size, rel_path_pos)) { if (*abs_path_pos >= abs_path_size - 1) return -ENOMEM; abs_path[*abs_path_pos] = rel_path[*rel_path_pos]; *rel_path_pos += 1; *abs_path_pos += 1; } /* Add a single backslash if next character is a backslash */ if (is_path_separator(rel_path, rel_path_size, rel_path_pos)) { *rel_path_pos += 1; if (*abs_path_pos < abs_path_size - 1) { abs_path[*abs_path_pos] = '\\'; *abs_path_pos += 1; } } return 0; } static int isobusfs_handle_relative_path(const char *rel_path, size_t rel_path_size, size_t *rel_path_pos, char *abs_path, size_t abs_path_size, size_t *abs_path_pos) { int ret; if (*abs_path_pos >= abs_path_size - 1) return -ENOMEM; /* Check for '.' or '..' followed by a backslash or at the end of the * string */ if (isobusfs_is_dot_directive(rel_path, rel_path_size, rel_path_pos)) { isobusfs_handle_dot_directive(rel_path, rel_path_size, rel_path_pos, abs_path, abs_path_pos); } else { /* Process normally for filenames or directories */ ret = isobusfs_process_path_segment(rel_path, rel_path_size, rel_path_pos, abs_path, abs_path_size, abs_path_pos); if (ret) return ret; } return 0; } int isobusfs_convert_relative_to_absolute(struct isobusfs_srv_priv *priv, const char *current_dir, const char *rel_path, size_t rel_path_size, char *abs_path, size_t abs_path_size) { size_t abs_path_pos = 0; size_t rel_path_pos = 0; size_t current_dir_len; int ret; if (!current_dir || !rel_path || !abs_path || !rel_path_size || !abs_path_size) return -EINVAL; ret = isobusfs_validate_path_chars(rel_path, rel_path_size); if (ret != 0) return ret; current_dir_len = strlen(current_dir); if (current_dir_len >= abs_path_size) return -ENOMEM; if (current_dir_len == 0) return -EINVAL; ret = isobusfs_handle_path_prefix(current_dir, current_dir_len, rel_path, rel_path_size, &rel_path_pos, abs_path, abs_path_size, &abs_path_pos); if (ret) return ret; while (rel_path_pos < rel_path_size && rel_path[rel_path_pos] != '\0') { ret = isobusfs_handle_relative_path(rel_path, rel_path_size, &rel_path_pos, abs_path, abs_path_size, &abs_path_pos); if (ret) return ret; } abs_path[abs_path_pos] = '\0'; return 0; } /* change current directory response function */ static int isobusfs_srv_dh_ccd_res(struct isobusfs_srv_priv *priv, struct isobusfs_msg *msg) { struct isobusfs_dh_ccd_req *req = (struct isobusfs_dh_ccd_req *)msg->buf; uint8_t error_code = ISOBUSFS_ERR_SUCCESS; struct isobusfs_srv_client *client; struct isobusfs_dh_ccd_res res; size_t abs_path_len; char *abs_path; int ret; /* * We assime, the relative path stored in res->name is not longer * than absolue path */ if (req->name_len > ISOBUSFS_SRV_MAX_PATH_LEN) { pr_warn("path too long"); return -EINVAL; } client = isobusfs_srv_get_client_by_msg(priv, msg); if (!client) { pr_warn("client not found"); return -ENOENT; } abs_path_len = ISOBUSFS_SRV_MAX_PATH_LEN; abs_path = malloc(abs_path_len); if (!abs_path) { pr_warn("failed to allocate memory"); return -ENOMEM; } pr_debug("< rx change current directory request from client 0x%2x: %.*s. Current directory: %s", client->addr, req->name_len, req->name, client->current_dir); /* Normalize provided string and convert it to absolute ISOBUS FS path */ ret = isobusfs_convert_relative_to_absolute(priv, client->current_dir, (char *)req->name, req->name_len, abs_path, abs_path_len); if (ret < 0) goto process_error; pr_debug("converted relative to absolute ISOBUS FS internal path: %s", abs_path); ret = isobusfs_check_current_dir_access(priv, abs_path, abs_path_len); process_error: if (ret < 0) { /* linux_error_to_isobusfs_error() can't distinguish between * -EINVAL vor SRC and DST, so we have to do it manually. */ if (ret == -EINVAL) error_code = ISOBUSFS_ERR_INVALID_DST_NAME; else error_code = linux_error_to_isobusfs_error(ret); } else { /* change current directory */ strncpy(client->current_dir, abs_path, ISOBUSFS_SRV_MAX_PATH_LEN); } res.fs_function = isobusfs_cg_function_to_buf(ISOBUSFS_CG_DIRECTORY_HANDLING, ISOBUSFS_DH_F_CHANGE_CURRENT_DIR_RES); res.tan = req->tan; res.error_code = error_code; memset(&res.reserved[0], 0xff, sizeof(res.reserved)); /* send to socket */ ret = isobusfs_srv_sendto(priv, msg, &res, sizeof(res)); if (ret < 0) { pr_warn("can't send current directory response"); goto free_abs_path; } pr_debug("> tx: ccd response. Error code: %d", error_code); free_abs_path: free(abs_path); return ret; } /* current directory response function */ /* Command group: directory handling */ int isobusfs_srv_rx_cg_dh(struct isobusfs_srv_priv *priv, struct isobusfs_msg *msg) { int func = isobusfs_buf_to_function(msg->buf); int ret = 0; switch (func) { case ISOBUSFS_DH_F_GET_CURRENT_DIR_REQ: return isobusfs_srv_dh_current_dir_res(priv, msg); case ISOBUSFS_DH_F_CHANGE_CURRENT_DIR_REQ: return isobusfs_srv_dh_ccd_res(priv, msg); default: goto not_supported; } return ret; not_supported: isobusfs_srv_send_error(priv, msg, ISOBUSFS_ERR_FUNC_NOT_SUPPORTED); pr_warn("%s: unsupported function: %i", __func__, func); /* Not a critical error */ return 0; }