blob: 2ce2248996e948a132b853b83b6a07fd557e85e1 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
#include <assert.h>
#include <string.h>
#include "testutil/testutil.h"
#include "nffs/nffs.h"
#include "nffs_priv.h"
static int
nffs_write_fill_crc16_overwrite(struct nffs_disk_block *disk_block,
uint8_t src_area_idx, uint32_t src_area_offset,
uint16_t left_copy_len, uint16_t right_copy_len,
const void *new_data, uint16_t new_data_len)
{
uint16_t block_off;
uint16_t crc16;
int rc;
block_off = 0;
crc16 = nffs_crc_disk_block_hdr(disk_block);
block_off += sizeof *disk_block;
/* Copy data from the start of the old block, in case the new data starts
* at a non-zero offset.
*/
if (left_copy_len > 0) {
rc = nffs_crc_flash(crc16, src_area_idx, src_area_offset + block_off,
left_copy_len, &crc16);
if (rc != 0) {
return rc;
}
block_off += left_copy_len;
}
/* Write the new data into the data block. This may extend the block's
* length beyond its old value.
*/
crc16 = crc16_ccitt(crc16, new_data, new_data_len);
block_off += new_data_len;
/* Copy data from the end of the old block, in case the new data doesn't
* extend to the end of the block.
*/
if (right_copy_len > 0) {
rc = nffs_crc_flash(crc16, src_area_idx, src_area_offset + block_off,
right_copy_len, &crc16);
if (rc != 0) {
return rc;
}
block_off += right_copy_len;
}
assert(block_off == sizeof *disk_block + disk_block->ndb_data_len);
disk_block->ndb_crc16 = crc16;
return 0;
}
/**
* Overwrites an existing data block. The resulting block has the same ID as
* the old one, but it supersedes it with a greater sequence number.
*
* @param entry The data block to overwrite.
* @param left_copy_len The number of bytes of existing data to retain
* before the new data begins.
* @param new_data The new data to write to the block.
* @param new_data_len The number of new bytes to write to the block.
* If this value plus left_copy_len is less
* than the existing block's data length,
* previous data at the end of the block is
* retained.
*
* @return 0 on success; nonzero on failure.
*/
static int
nffs_write_over_block(struct nffs_hash_entry *entry, uint16_t left_copy_len,
const void *new_data, uint16_t new_data_len)
{
struct nffs_disk_block disk_block;
struct nffs_block block;
uint32_t src_area_offset;
uint32_t dst_area_offset;
uint16_t right_copy_len;
uint16_t block_off;
uint8_t src_area_idx;
uint8_t dst_area_idx;
int rc;
rc = nffs_block_from_hash_entry(&block, entry);
if (rc != 0) {
return rc;
}
assert(left_copy_len <= block.nb_data_len);
/* Determine how much old data at the end of the block needs to be
* retained. If the new data doesn't extend to the end of the block, the
* the rest of the block retains its old contents.
*/
if (left_copy_len + new_data_len > block.nb_data_len) {
right_copy_len = 0;
} else {
right_copy_len = block.nb_data_len - left_copy_len - new_data_len;
}
block.nb_seq++;
block.nb_data_len = left_copy_len + new_data_len + right_copy_len;
nffs_block_to_disk(&block, &disk_block);
nffs_flash_loc_expand(entry->nhe_flash_loc,
&src_area_idx, &src_area_offset);
rc = nffs_write_fill_crc16_overwrite(&disk_block,
src_area_idx, src_area_offset,
left_copy_len, right_copy_len,
new_data, new_data_len);
if (rc != 0) {
return rc;
}
rc = nffs_misc_reserve_space(sizeof disk_block + disk_block.ndb_data_len,
&dst_area_idx, &dst_area_offset);
if (rc != 0) {
return rc;
}
block_off = 0;
/* Write the block header. */
rc = nffs_flash_write(dst_area_idx, dst_area_offset + block_off,
&disk_block, sizeof disk_block);
if (rc != 0) {
return rc;
}
block_off += sizeof disk_block;
/* Copy data from the start of the old block, in case the new data starts
* at a non-zero offset.
*/
if (left_copy_len > 0) {
rc = nffs_flash_copy(src_area_idx, src_area_offset + block_off,
dst_area_idx, dst_area_offset + block_off,
left_copy_len);
if (rc != 0) {
return rc;
}
block_off += left_copy_len;
}
/* Write the new data into the data block. This may extend the block's
* length beyond its old value.
*/
rc = nffs_flash_write(dst_area_idx, dst_area_offset + block_off,
new_data, new_data_len);
if (rc != 0) {
return rc;
}
block_off += new_data_len;
/* Copy data from the end of the old block, in case the new data doesn't
* extend to the end of the block.
*/
if (right_copy_len > 0) {
rc = nffs_flash_copy(src_area_idx, src_area_offset + block_off,
dst_area_idx, dst_area_offset + block_off,
right_copy_len);
if (rc != 0) {
return rc;
}
block_off += right_copy_len;
}
assert(block_off == sizeof disk_block + block.nb_data_len);
entry->nhe_flash_loc = nffs_flash_loc(dst_area_idx, dst_area_offset);
ASSERT_IF_TEST(nffs_crc_disk_block_validate(&disk_block, dst_area_idx,
dst_area_offset) == 0);
return 0;
}
/**
* Appends a new block to an inode block chain.
*
* @param inode_entry The inode to append a block to.
* @param data The contents of the new block.
* @param len The number of bytes of data to write.
*
* @return 0 on success; nonzero on failure.
*/
static int
nffs_write_append(struct nffs_cache_inode *cache_inode, const void *data,
uint16_t len)
{
struct nffs_inode_entry *inode_entry;
struct nffs_hash_entry *entry;
struct nffs_disk_block disk_block;
uint32_t area_offset;
uint8_t area_idx;
int rc;
rc = nffs_block_entry_reserve(&entry);
if (entry == NULL) {
return FS_ENOMEM;
}
inode_entry = cache_inode->nci_inode.ni_inode_entry;
memset(&disk_block, 0, sizeof disk_block);
disk_block.ndb_id = nffs_hash_next_block_id++;
disk_block.ndb_seq = 0;
disk_block.ndb_inode_id = inode_entry->nie_hash_entry.nhe_id;
if (inode_entry->nie_last_block_entry == NULL) {
disk_block.ndb_prev_id = NFFS_ID_NONE;
} else {
disk_block.ndb_prev_id = inode_entry->nie_last_block_entry->nhe_id;
}
disk_block.ndb_data_len = len;
nffs_crc_disk_block_fill(&disk_block, data);
rc = nffs_block_write_disk(&disk_block, data, &area_idx, &area_offset);
if (rc != 0) {
return rc;
}
entry->nhe_id = disk_block.ndb_id;
entry->nhe_flash_loc = nffs_flash_loc(area_idx, area_offset);
nffs_hash_insert(entry);
inode_entry->nie_last_block_entry = entry;
/*
* When a new block is appended to a file, the inode must be written
* out to flash so the last block id is is stored persistently.
* Need to be written atomically with writing the block out so filesystem
* remains consistent. nffs_lock is held in nffs_write().
* The inode will not be updated if it's been unlinked on disk and this
* is signaled by setting the hash entry's flash location to NONE
*/
if (inode_entry->nie_hash_entry.nhe_flash_loc != NFFS_FLASH_LOC_NONE) {
rc = nffs_inode_update(inode_entry);
}
/* Update cached inode with the new file size. */
cache_inode->nci_file_size += len;
/* Add appended block to the cache. */
nffs_cache_seek(cache_inode, cache_inode->nci_file_size - 1, NULL);
return 0;
}
/**
* Performs a single write operation. The data written must be no greater
* than the maximum block data length. If old data gets overwritten, then
* the existing data blocks are superseded as necessary.
*
* @param write_info Describes the write operation being perfomred.
* @param inode_entry The file inode to write to.
* @param data The new data to write.
* @param data_len The number of bytes of new data to write.
*
* @return 0 on success; nonzero on failure.
*/
static int
nffs_write_chunk(struct nffs_inode_entry *inode_entry, uint32_t file_offset,
const void *data, uint16_t data_len)
{
struct nffs_cache_inode *cache_inode;
struct nffs_cache_block *cache_block;
unsigned int gc_count;
uint32_t append_len;
uint32_t data_offset;
uint32_t block_end;
uint32_t dst_off;
uint16_t chunk_off;
uint16_t chunk_sz;
int rc;
assert(data_len <= nffs_block_max_data_sz);
rc = nffs_cache_inode_ensure(&cache_inode, inode_entry);
if (rc != 0) {
return rc;
}
/** Handle the simple append case first. */
if (file_offset == cache_inode->nci_file_size) {
rc = nffs_write_append(cache_inode, data, data_len);
return rc;
}
/** This is not an append; i.e., old data is getting overwritten. */
dst_off = file_offset + data_len;
data_offset = data_len;
cache_block = NULL;
if (dst_off > cache_inode->nci_file_size) {
append_len = dst_off - cache_inode->nci_file_size;
} else {
append_len = 0;
}
do {
if (cache_block == NULL) {
rc = nffs_cache_seek(cache_inode, dst_off - 1, &cache_block);
if (rc != 0) {
return rc;
}
}
if (cache_block->ncb_file_offset < file_offset) {
chunk_off = file_offset - cache_block->ncb_file_offset;
} else {
chunk_off = 0;
}
chunk_sz = cache_block->ncb_block.nb_data_len - chunk_off;
block_end = cache_block->ncb_file_offset +
cache_block->ncb_block.nb_data_len;
if (block_end != dst_off) {
chunk_sz += (int)(dst_off - block_end);
}
/* Remember the current garbage collection count. If the count
* increases during the write, then the block cache has been
* invalidated and we need to reset our pointers.
*/
gc_count = nffs_gc_count;
data_offset = cache_block->ncb_file_offset + chunk_off - file_offset;
rc = nffs_write_over_block(cache_block->ncb_block.nb_hash_entry,
chunk_off, data + data_offset, chunk_sz);
if (rc != 0) {
return rc;
}
dst_off -= chunk_sz;
if (gc_count != nffs_gc_count) {
/* Garbage collection occurred; the current cached block pointer is
* invalid, so reset it. The cached inode is still valid.
*/
cache_block = NULL;
} else {
cache_block = TAILQ_PREV(cache_block, nffs_cache_block_list,
ncb_link);
}
} while (data_offset > 0);
cache_inode->nci_file_size += append_len;
return 0;
}
/**
* Writes a chunk of contiguous data to a file.
*
* @param file The file to write to.
* @param data The data to write.
* @param len The length of data to write.
*
* @return 0 on success; nonzero on failure.
*/
int
nffs_write_to_file(struct nffs_file *file, const void *data, int len)
{
struct nffs_cache_inode *cache_inode;
const uint8_t *data_ptr;
uint16_t chunk_size;
int rc;
if (!(file->nf_access_flags & FS_ACCESS_WRITE)) {
return FS_EACCESS;
}
if (len == 0) {
return 0;
}
rc = nffs_cache_inode_ensure(&cache_inode, file->nf_inode_entry);
if (rc != 0) {
return rc;
}
/* The append flag forces all writes to the end of the file, regardless of
* seek position.
*/
if (file->nf_access_flags & FS_ACCESS_APPEND) {
file->nf_offset = cache_inode->nci_file_size;
}
/* Write data as a sequence of blocks. */
data_ptr = data;
while (len > 0) {
if (len > nffs_block_max_data_sz) {
chunk_size = nffs_block_max_data_sz;
} else {
chunk_size = len;
}
rc = nffs_write_chunk(file->nf_inode_entry, file->nf_offset, data_ptr,
chunk_size);
if (rc != 0) {
return rc;
}
len -= chunk_size;
data_ptr += chunk_size;
file->nf_offset += chunk_size;
}
return 0;
}