| /** |
| * dir.c |
| * |
| * Many parts of codes are copied from Linux kernel/fs/f2fs. |
| * |
| * Copyright (C) 2015 Huawei Ltd. |
| * Witten by: |
| * Hou Pengyang <houpengyang@huawei.com> |
| * Liu Shuoran <liushuoran@huawei.com> |
| * Jaegeuk Kim <jaegeuk@kernel.org> |
| * |
| * This program is free software; you can redistribute it and/or modify |
| * it under the terms of the GNU General Public License version 2 as |
| * published by the Free Software Foundation. |
| */ |
| #include "fsck.h" |
| #include "node.h" |
| |
| static int room_for_filename(const u8 *bitmap, int slots, int max_slots) |
| { |
| int bit_start = 0; |
| int zero_start, zero_end; |
| next: |
| zero_start = find_next_zero_bit_le(bitmap, max_slots, bit_start); |
| if (zero_start >= max_slots) |
| return max_slots; |
| |
| zero_end = find_next_bit_le(bitmap, max_slots, zero_start + 1); |
| |
| if (zero_end - zero_start >= slots) |
| return zero_start; |
| bit_start = zero_end; |
| goto next; |
| |
| } |
| |
| static void make_dentry_ptr(struct f2fs_dentry_ptr *d, void *src, int type) |
| { |
| if (type == 1) { |
| struct f2fs_dentry_block *t = (struct f2fs_dentry_block *)src; |
| d->max = NR_DENTRY_IN_BLOCK; |
| d->bitmap = t->dentry_bitmap; |
| d->dentry = t->dentry; |
| d->filename = t->filename; |
| } else { |
| struct f2fs_inline_dentry *t = (struct f2fs_inline_dentry *)src; |
| d->max = NR_INLINE_DENTRY; |
| d->bitmap = t->dentry_bitmap; |
| d->dentry = t->dentry; |
| d->filename = t->filename; |
| } |
| } |
| |
| static struct f2fs_dir_entry *find_target_dentry(const u8 *name, |
| unsigned int len, f2fs_hash_t namehash, int *max_slots, |
| struct f2fs_dentry_ptr *d) |
| { |
| struct f2fs_dir_entry *de; |
| unsigned long bit_pos = 0; |
| int max_len = 0; |
| |
| if (max_slots) |
| *max_slots = 0; |
| while (bit_pos < d->max) { |
| if (!test_bit_le(bit_pos, d->bitmap)) { |
| bit_pos++; |
| max_len++; |
| continue; |
| } |
| |
| de = &d->dentry[bit_pos]; |
| if (le16_to_cpu(de->name_len) == len && |
| de->hash_code == namehash && |
| !memcmp(d->filename[bit_pos], name, len)) { |
| goto found; |
| } |
| |
| if (max_slots && max_len > *max_slots) |
| *max_slots = max_len; |
| max_len = 0; |
| bit_pos += GET_DENTRY_SLOTS(le16_to_cpu(de->name_len)); |
| } |
| de = NULL; |
| found: |
| if (max_slots && max_len > *max_slots) |
| *max_slots = max_len; |
| return de; |
| } |
| |
| static struct f2fs_dir_entry *find_in_block(void *block, |
| const u8 *name, int len, f2fs_hash_t namehash, |
| int *max_slots) |
| { |
| struct f2fs_dentry_ptr d; |
| |
| make_dentry_ptr(&d, block, 1); |
| return find_target_dentry(name, len, namehash, max_slots, &d); |
| } |
| |
| static int find_in_level(struct f2fs_sb_info *sbi,struct f2fs_node *dir, |
| unsigned int level, struct dentry *de) |
| { |
| unsigned int nbucket, nblock; |
| unsigned int bidx, end_block; |
| struct f2fs_dir_entry *dentry = NULL; |
| struct dnode_of_data dn = {0}; |
| void *dentry_blk; |
| int max_slots = 214; |
| nid_t ino = le32_to_cpu(dir->footer.ino); |
| f2fs_hash_t namehash; |
| unsigned int dir_level = dir->i.i_dir_level; |
| int ret = 0; |
| |
| namehash = f2fs_dentry_hash(de->name, de->len); |
| |
| nbucket = dir_buckets(level, dir_level); |
| nblock = bucket_blocks(level); |
| |
| bidx = dir_block_index(level, dir_level, le32_to_cpu(namehash) % nbucket); |
| end_block = bidx + nblock; |
| |
| dentry_blk = calloc(BLOCK_SZ, 1); |
| ASSERT(dentry_blk); |
| |
| for (; bidx < end_block; bidx++) { |
| |
| /* Firstly, we should know direct node of target data blk */ |
| if (dn.node_blk && dn.node_blk != dn.inode_blk) |
| free(dn.node_blk); |
| |
| set_new_dnode(&dn, dir, NULL, ino); |
| get_dnode_of_data(sbi, &dn, bidx, LOOKUP_NODE); |
| if (dn.data_blkaddr == NULL_ADDR) |
| continue; |
| |
| ret = dev_read_block(dentry_blk, dn.data_blkaddr); |
| ASSERT(ret >= 0); |
| |
| dentry = find_in_block(dentry_blk, de->name, de->len, |
| namehash, &max_slots); |
| if (dentry) { |
| ret = 1; |
| de->ino = le32_to_cpu(dentry->ino); |
| break; |
| } |
| } |
| |
| if (dn.node_blk && dn.node_blk != dn.inode_blk) |
| free(dn.node_blk); |
| free(dentry_blk); |
| |
| return ret; |
| } |
| |
| static int f2fs_find_entry(struct f2fs_sb_info *sbi, |
| struct f2fs_node *dir, struct dentry *de) |
| { |
| unsigned int max_depth; |
| unsigned int level; |
| |
| max_depth = le32_to_cpu(dir->i.i_current_depth); |
| for (level = 0; level < max_depth; level ++) { |
| if (find_in_level(sbi, dir, level, de)) |
| return 1; |
| } |
| return 0; |
| } |
| |
| static void f2fs_update_dentry(nid_t ino, int file_type, |
| struct f2fs_dentry_ptr *d, |
| const unsigned char *name, int len, f2fs_hash_t name_hash, |
| unsigned int bit_pos) |
| { |
| struct f2fs_dir_entry *de; |
| int slots = GET_DENTRY_SLOTS(len); |
| int i; |
| |
| de = &d->dentry[bit_pos]; |
| de->name_len = cpu_to_le16(len); |
| de->hash_code = name_hash; |
| memcpy(d->filename[bit_pos], name, len); |
| d->filename[bit_pos][len] = 0; |
| de->ino = cpu_to_le32(ino); |
| de->file_type = file_type; |
| for (i = 0; i < slots; i++) |
| test_and_set_bit_le(bit_pos + i, d->bitmap); |
| } |
| |
| /* |
| * f2fs_add_link - Add a new file(dir) to parent dir. |
| */ |
| static int f2fs_add_link(struct f2fs_sb_info *sbi, struct f2fs_node *parent, |
| const unsigned char *name, int name_len, nid_t ino, |
| int file_type, block_t p_blkaddr, int inc_link) |
| { |
| int level = 0, current_depth, bit_pos; |
| int nbucket, nblock, bidx, block; |
| int slots = GET_DENTRY_SLOTS(name_len); |
| f2fs_hash_t dentry_hash = f2fs_dentry_hash(name, name_len); |
| struct f2fs_dentry_block *dentry_blk; |
| struct f2fs_dentry_ptr d; |
| struct dnode_of_data dn = {0}; |
| nid_t pino = le32_to_cpu(parent->footer.ino); |
| unsigned int dir_level = parent->i.i_dir_level; |
| int ret; |
| |
| if (parent == NULL) |
| return -EINVAL; |
| |
| if (!pino) { |
| ERR_MSG("Wrong parent ino:%d \n", pino); |
| return -EINVAL; |
| } |
| |
| dentry_blk = calloc(BLOCK_SZ, 1); |
| ASSERT(dentry_blk); |
| |
| current_depth = le32_to_cpu(parent->i.i_current_depth); |
| start: |
| if (current_depth == MAX_DIR_HASH_DEPTH) { |
| free(dentry_blk); |
| ERR_MSG("\tError: MAX_DIR_HASH\n"); |
| return -ENOSPC; |
| } |
| |
| /* Need a new dentry block */ |
| if (level == current_depth) |
| ++current_depth; |
| |
| nbucket = dir_buckets(level, dir_level); |
| nblock = bucket_blocks(level); |
| bidx = dir_block_index(level, dir_level, le32_to_cpu(dentry_hash) % nbucket); |
| |
| for (block = bidx; block <= (bidx + nblock - 1); block++) { |
| |
| /* Firstly, we should know the direct node of target data blk */ |
| if (dn.node_blk && dn.node_blk != dn.inode_blk) |
| free(dn.node_blk); |
| |
| set_new_dnode(&dn, parent, NULL, pino); |
| get_dnode_of_data(sbi, &dn, block, ALLOC_NODE); |
| |
| if (dn.data_blkaddr == NULL_ADDR) { |
| new_data_block(sbi, dentry_blk, &dn, CURSEG_HOT_DATA); |
| } else { |
| ret = dev_read_block(dentry_blk, dn.data_blkaddr); |
| ASSERT(ret >= 0); |
| } |
| bit_pos = room_for_filename(dentry_blk->dentry_bitmap, |
| slots, NR_DENTRY_IN_BLOCK); |
| |
| if (bit_pos < NR_DENTRY_IN_BLOCK) |
| goto add_dentry; |
| } |
| level ++; |
| goto start; |
| |
| add_dentry: |
| make_dentry_ptr(&d, (void *)dentry_blk, 1); |
| f2fs_update_dentry(ino, file_type, &d, name, name_len, dentry_hash, bit_pos); |
| |
| ret = dev_write_block(dentry_blk, dn.data_blkaddr); |
| ASSERT(ret >= 0); |
| |
| /* |
| * Parent inode needs updating, because its inode info may be changed. |
| * such as i_current_depth and i_blocks. |
| */ |
| if (parent->i.i_current_depth != cpu_to_le32(current_depth)) { |
| parent->i.i_current_depth = cpu_to_le32(current_depth); |
| dn.idirty = 1; |
| } |
| |
| /* Update parent's i_links info*/ |
| if (inc_link && (file_type == F2FS_FT_DIR)){ |
| u32 links = le32_to_cpu(parent->i.i_links); |
| parent->i.i_links = cpu_to_le32(links + 1); |
| dn.idirty = 1; |
| } |
| |
| if ((block + 1) * F2FS_BLKSIZE > le64_to_cpu(parent->i.i_size)) { |
| parent->i.i_size = cpu_to_le64((block + 1) * F2FS_BLKSIZE); |
| dn.idirty = 1; |
| } |
| |
| if (dn.ndirty) { |
| ret = dev_write_block(dn.node_blk, dn.node_blkaddr); |
| ASSERT(ret >= 0); |
| } |
| |
| if (dn.idirty) { |
| ASSERT(parent == dn.inode_blk); |
| ret = dev_write_block(dn.inode_blk, p_blkaddr); |
| ASSERT(ret >= 0); |
| } |
| |
| if (dn.node_blk != dn.inode_blk) |
| free(dn.node_blk); |
| free(dentry_blk); |
| return 0; |
| } |
| |
| static void make_empty_dir(struct f2fs_sb_info *sbi, struct f2fs_node *inode) |
| { |
| struct f2fs_dentry_block *dent_blk; |
| nid_t ino = le32_to_cpu(inode->footer.ino); |
| nid_t pino = le32_to_cpu(inode->i.i_pino); |
| struct f2fs_summary sum; |
| struct node_info ni; |
| block_t blkaddr; |
| int ret; |
| |
| get_node_info(sbi, ino, &ni); |
| |
| dent_blk = calloc(BLOCK_SZ, 1); |
| ASSERT(dent_blk); |
| |
| dent_blk->dentry[0].hash_code = 0; |
| dent_blk->dentry[0].ino = cpu_to_le32(ino); |
| dent_blk->dentry[0].name_len = cpu_to_le16(1); |
| dent_blk->dentry[0].file_type = F2FS_FT_DIR; |
| memcpy(dent_blk->filename[0], ".", 1); |
| |
| dent_blk->dentry[1].hash_code = 0; |
| dent_blk->dentry[1].ino = cpu_to_le32(pino); |
| dent_blk->dentry[1].name_len = cpu_to_le16(2); |
| dent_blk->dentry[1].file_type = F2FS_FT_DIR; |
| memcpy(dent_blk->filename[1], "..", 2); |
| |
| test_and_set_bit_le(0, dent_blk->dentry_bitmap); |
| test_and_set_bit_le(1, dent_blk->dentry_bitmap); |
| |
| set_summary(&sum, ino, 0, ni.version); |
| reserve_new_block(sbi, &blkaddr, &sum, CURSEG_HOT_DATA); |
| |
| ret = dev_write_block(dent_blk, blkaddr); |
| ASSERT(ret >= 0); |
| |
| inode->i.i_addr[0] = cpu_to_le32(blkaddr); |
| free(dent_blk); |
| } |
| |
| static void page_symlink(struct f2fs_sb_info *sbi, struct f2fs_node *inode, |
| const char *symname, int symlen) |
| { |
| nid_t ino = le32_to_cpu(inode->footer.ino); |
| struct f2fs_summary sum; |
| struct node_info ni; |
| char *data_blk; |
| block_t blkaddr; |
| int ret; |
| |
| get_node_info(sbi, ino, &ni); |
| |
| /* store into inline_data */ |
| if (symlen + 1 <= MAX_INLINE_DATA) { |
| inode->i.i_inline |= F2FS_INLINE_DATA; |
| inode->i.i_inline |= F2FS_DATA_EXIST; |
| memcpy(&inode->i.i_addr[1], symname, symlen); |
| return; |
| } |
| |
| data_blk = calloc(BLOCK_SZ, 1); |
| ASSERT(data_blk); |
| |
| memcpy(data_blk, symname, symlen); |
| |
| set_summary(&sum, ino, 0, ni.version); |
| reserve_new_block(sbi, &blkaddr, &sum, CURSEG_WARM_DATA); |
| |
| ret = dev_write_block(data_blk, blkaddr); |
| ASSERT(ret >= 0); |
| |
| inode->i.i_addr[0] = cpu_to_le32(blkaddr); |
| free(data_blk); |
| } |
| |
| static void init_inode_block(struct f2fs_sb_info *sbi, |
| struct f2fs_node *node_blk, struct dentry *de) |
| { |
| struct f2fs_checkpoint *ckpt = F2FS_CKPT(sbi); |
| mode_t mode = de->mode; |
| int links = 1; |
| unsigned int size; |
| int blocks = 1; |
| |
| if (de->file_type == F2FS_FT_DIR) { |
| mode |= S_IFDIR; |
| size = 4096; |
| links++; |
| blocks++; |
| } else if (de->file_type == F2FS_FT_REG_FILE) { |
| mode |= S_IFREG; |
| size = 0; |
| } else if (de->file_type == F2FS_FT_SYMLINK) { |
| ASSERT(de->link); |
| mode |= S_IFLNK; |
| size = strlen(de->link); |
| if (size + 1 > MAX_INLINE_DATA) |
| blocks++; |
| } else { |
| ASSERT(0); |
| } |
| |
| node_blk->i.i_mode = cpu_to_le16(mode); |
| node_blk->i.i_advise = 0; |
| node_blk->i.i_uid = cpu_to_le32(de->uid); |
| node_blk->i.i_gid = cpu_to_le32(de->gid); |
| node_blk->i.i_links = cpu_to_le32(links); |
| node_blk->i.i_size = cpu_to_le32(size); |
| node_blk->i.i_blocks = cpu_to_le32(blocks); |
| node_blk->i.i_atime = cpu_to_le64(de->mtime); |
| node_blk->i.i_ctime = cpu_to_le64(de->mtime); |
| node_blk->i.i_mtime = cpu_to_le64(de->mtime); |
| node_blk->i.i_atime_nsec = 0; |
| node_blk->i.i_ctime_nsec = 0; |
| node_blk->i.i_mtime_nsec = 0; |
| node_blk->i.i_generation = 0; |
| node_blk->i.i_current_depth = cpu_to_le32(1); |
| node_blk->i.i_xattr_nid = 0; |
| node_blk->i.i_flags = 0; |
| node_blk->i.i_inline = F2FS_INLINE_XATTR; |
| node_blk->i.i_pino = cpu_to_le32(de->pino); |
| node_blk->i.i_namelen = cpu_to_le32(de->len); |
| memcpy(node_blk->i.i_name, de->name, de->len); |
| node_blk->i.i_name[de->len] = 0; |
| |
| node_blk->footer.ino = cpu_to_le32(de->ino); |
| node_blk->footer.nid = cpu_to_le32(de->ino); |
| node_blk->footer.flag = 0; |
| node_blk->footer.cp_ver = ckpt->checkpoint_ver; |
| |
| if (S_ISDIR(mode)) |
| make_empty_dir(sbi, node_blk); |
| else if (S_ISLNK(mode)) |
| page_symlink(sbi, node_blk, de->link, size); |
| } |
| |
| int convert_inline_dentry(struct f2fs_sb_info *sbi, struct f2fs_node *node, |
| block_t p_blkaddr) |
| { |
| struct f2fs_inode *inode = &(node->i); |
| unsigned int dir_level = node->i.i_dir_level; |
| nid_t ino = le32_to_cpu(node->footer.ino); |
| char inline_data[MAX_INLINE_DATA]; |
| struct dnode_of_data dn = {0}; |
| struct f2fs_dentry_ptr d; |
| unsigned long bit_pos = 0; |
| int ret = 0; |
| |
| if (!(inode->i_inline & F2FS_INLINE_DENTRY)) |
| return 0; |
| |
| memcpy(inline_data, inline_data_addr(node), MAX_INLINE_DATA); |
| memset(inline_data_addr(node), 0, MAX_INLINE_DATA); |
| inode->i_inline &= ~F2FS_INLINE_DENTRY; |
| |
| ret = dev_write_block(node, p_blkaddr); |
| ASSERT(ret >= 0); |
| |
| if (!dir_level) { |
| struct f2fs_inline_dentry *inline_dentry; |
| struct f2fs_dentry_block *dentry_blk; |
| |
| dentry_blk = calloc(BLOCK_SZ, 1); |
| ASSERT(dentry_blk); |
| |
| set_new_dnode(&dn, node, NULL, ino); |
| get_dnode_of_data(sbi, &dn, 0, ALLOC_NODE); |
| if (dn.data_blkaddr == NULL_ADDR) |
| new_data_block(sbi, dentry_blk, &dn, CURSEG_HOT_DATA); |
| |
| inline_dentry = (struct f2fs_inline_dentry *)inline_data; |
| /* copy data from inline dentry block to new dentry block */ |
| memcpy(dentry_blk->dentry_bitmap, inline_dentry->dentry_bitmap, |
| INLINE_DENTRY_BITMAP_SIZE); |
| memset(dentry_blk->dentry_bitmap + INLINE_DENTRY_BITMAP_SIZE, 0, |
| SIZE_OF_DENTRY_BITMAP - INLINE_DENTRY_BITMAP_SIZE); |
| |
| memcpy(dentry_blk->dentry, inline_dentry->dentry, |
| sizeof(struct f2fs_dir_entry) * NR_INLINE_DENTRY); |
| memcpy(dentry_blk->filename, inline_dentry->filename, |
| NR_INLINE_DENTRY * F2FS_SLOT_LEN); |
| |
| ret = dev_write_block(dentry_blk, dn.data_blkaddr); |
| ASSERT(ret >= 0); |
| |
| MSG(1, "%s: copy inline entry to block\n", __func__); |
| |
| free(dentry_blk); |
| return ret; |
| } |
| |
| make_empty_dir(sbi, node); |
| make_dentry_ptr(&d, (void *)inline_data, 2); |
| |
| while (bit_pos < d.max) { |
| struct f2fs_dir_entry *de; |
| const unsigned char *filename; |
| int namelen; |
| |
| if (!test_bit_le(bit_pos, d.bitmap)) { |
| bit_pos++; |
| continue; |
| } |
| |
| de = &d.dentry[bit_pos]; |
| if (!de->name_len) { |
| bit_pos++; |
| continue; |
| } |
| |
| filename = d.filename[bit_pos]; |
| namelen = le32_to_cpu(de->name_len); |
| |
| if (is_dot_dotdot(filename, namelen)) { |
| bit_pos += GET_DENTRY_SLOTS(namelen); |
| continue; |
| } |
| |
| ret = f2fs_add_link(sbi, node, filename, namelen, |
| le32_to_cpu(de->ino), |
| de->file_type, p_blkaddr, 0); |
| if (ret) |
| MSG(0, "Convert file \"%s\" ERR=%d\n", filename, ret); |
| else |
| MSG(1, "%s: add inline entry to block\n", __func__); |
| |
| bit_pos += GET_DENTRY_SLOTS(namelen); |
| } |
| |
| return 0; |
| } |
| |
| int f2fs_create(struct f2fs_sb_info *sbi, struct dentry *de) |
| { |
| struct f2fs_node *parent, *child; |
| struct node_info ni; |
| struct f2fs_summary sum; |
| block_t blkaddr; |
| int ret; |
| |
| /* Find if there is a */ |
| get_node_info(sbi, de->pino, &ni); |
| if (ni.blk_addr == NULL_ADDR) { |
| MSG(0, "No parent directory pino=%x\n", de->pino); |
| return -1; |
| } |
| |
| parent = calloc(BLOCK_SZ, 1); |
| ASSERT(parent); |
| |
| ret = dev_read_block(parent, ni.blk_addr); |
| ASSERT(ret >= 0); |
| |
| /* Must convert inline dentry before the following opertions */ |
| ret = convert_inline_dentry(sbi, parent, ni.blk_addr); |
| if (ret) { |
| MSG(0, "Convert inline dentry for pino=%x failed.\n", de->pino); |
| return -1; |
| } |
| |
| ret = f2fs_find_entry(sbi, parent, de); |
| if (ret) { |
| MSG(0, "Skip the existing \"%s\" pino=%x ERR=%d\n", |
| de->name, de->pino, ret); |
| if (de->file_type == F2FS_FT_REG_FILE) |
| de->ino = 0; |
| goto free_parent_dir; |
| } |
| |
| child = calloc(BLOCK_SZ, 1); |
| ASSERT(child); |
| |
| f2fs_alloc_nid(sbi, &de->ino, 1); |
| |
| init_inode_block(sbi, child, de); |
| |
| ret = f2fs_add_link(sbi, parent, child->i.i_name, |
| le32_to_cpu(child->i.i_namelen), |
| le32_to_cpu(child->footer.ino), |
| map_de_type(le16_to_cpu(child->i.i_mode)), |
| ni.blk_addr, 1); |
| if (ret) { |
| MSG(0, "Skip the existing \"%s\" pino=%x ERR=%d\n", |
| de->name, de->pino, ret); |
| goto free_child_dir; |
| } |
| |
| /* write child */ |
| set_summary(&sum, de->ino, 0, ni.version); |
| reserve_new_block(sbi, &blkaddr, &sum, CURSEG_HOT_NODE); |
| |
| /* update nat info */ |
| update_nat_blkaddr(sbi, de->ino, de->ino, blkaddr); |
| |
| ret = dev_write_block(child, blkaddr); |
| ASSERT(ret >= 0); |
| |
| update_free_segments(sbi); |
| MSG(1, "Info: Create \"%s\" type=%x, ino=%x / %x into \"%s\"\n", |
| de->full_path, de->file_type, |
| de->ino, de->pino, de->path); |
| free_child_dir: |
| free(child); |
| free_parent_dir: |
| free(parent); |
| return 0; |
| } |
| |
| int f2fs_mkdir(struct f2fs_sb_info *sbi, struct dentry *de) |
| { |
| return f2fs_create(sbi, de); |
| } |
| |
| int f2fs_symlink(struct f2fs_sb_info *sbi, struct dentry *de) |
| { |
| return f2fs_create(sbi, de); |
| } |
| |
| int f2fs_find_path(struct f2fs_sb_info *sbi, char *path, nid_t *ino) |
| { |
| struct f2fs_node *parent; |
| struct node_info ni; |
| struct dentry de; |
| int err = 0; |
| int ret; |
| char *p; |
| |
| if (path[0] != '/') |
| return -ENOENT; |
| |
| *ino = F2FS_ROOT_INO(sbi); |
| parent = calloc(BLOCK_SZ, 1); |
| ASSERT(parent); |
| |
| p = strtok(path, "/"); |
| while (p) { |
| de.name = (const u8 *)p; |
| de.len = strlen(p); |
| |
| get_node_info(sbi, *ino, &ni); |
| if (ni.blk_addr == NULL_ADDR) { |
| err = -ENOENT; |
| goto err; |
| } |
| ret = dev_read_block(parent, ni.blk_addr); |
| ASSERT(ret >= 0); |
| |
| ret = f2fs_find_entry(sbi, parent, &de); |
| if (!ret) { |
| err = -ENOENT; |
| goto err; |
| } |
| |
| *ino = de.ino; |
| p = strtok(NULL, "/"); |
| } |
| err: |
| free(parent); |
| return err; |
| } |