Commit 86a44e90 authored by Linus Torvalds's avatar Linus Torvalds

Merge tag 'ntfs3_for_5.15' of git://github.com/Paragon-Software-Group/linux-ntfs3

Pull ntfs3 fixes from Konstantin Komarov:
 "Use the new api for mounting as requested by Christoph.

  Also fixed:

   - some memory leaks and panic

   - xfstests (tested on x86_64) generic/016 generic/021 generic/022
     generic/041 generic/274 generic/423

   - some typos, wrong returned error codes, dead code, etc"

* tag 'ntfs3_for_5.15' of git://github.com/Paragon-Software-Group/linux-ntfs3: (70 commits)
  fs/ntfs3: Check for NULL pointers in ni_try_remove_attr_list
  fs/ntfs3: Refactor ntfs_read_mft
  fs/ntfs3: Refactor ni_parse_reparse
  fs/ntfs3: Refactor ntfs_create_inode
  fs/ntfs3: Refactor ntfs_readlink_hlp
  fs/ntfs3: Rework ntfs_utf16_to_nls
  fs/ntfs3: Fix memory leak if fill_super failed
  fs/ntfs3: Keep prealloc for all types of files
  fs/ntfs3: Remove unnecessary functions
  fs/ntfs3: Forbid FALLOC_FL_PUNCH_HOLE for normal files
  fs/ntfs3: Refactoring of ntfs_set_ea
  fs/ntfs3: Remove locked argument in ntfs_set_ea
  fs/ntfs3: Use available posix_acl_release instead of ntfs_posix_acl_release
  fs/ntfs3: Check for NULL if ATTR_EA_INFO is incorrect
  fs/ntfs3: Refactoring of ntfs_init_from_boot
  fs/ntfs3: Reject mount if boot's cluster size < media sector size
  fs/ntfs3: Refactoring lock in ntfs_init_acl
  fs/ntfs3: Change posix_acl_equiv_mode to posix_acl_update_mode
  fs/ntfs3: Pass flags to ntfs_set_ea in ntfs_set_acl_ex
  fs/ntfs3: Refactor ntfs_get_acl_ex for better readability
  ...
parents ec681c53 8607954c
......@@ -4,103 +4,112 @@
NTFS3
=====
Summary and Features
====================
NTFS3 is fully functional NTFS Read-Write driver. The driver works with
NTFS versions up to 3.1, normal/compressed/sparse files
and journal replaying. File system type to use on mount is 'ntfs3'.
NTFS3 is fully functional NTFS Read-Write driver. The driver works with NTFS
versions up to 3.1. File system type to use on mount is *ntfs3*.
- This driver implements NTFS read/write support for normal, sparse and
compressed files.
- Supports native journal replaying;
- Supports extended attributes
Predefined extended attributes:
- 'system.ntfs_security' gets/sets security
descriptor (SECURITY_DESCRIPTOR_RELATIVE)
- 'system.ntfs_attrib' gets/sets ntfs file/dir attributes.
Note: applied to empty files, this allows to switch type between
sparse(0x200), compressed(0x800) and normal;
- Supports native journal replaying.
- Supports NFS export of mounted NTFS volumes.
- Supports extended attributes. Predefined extended attributes:
- *system.ntfs_security* gets/sets security
Descriptor: SECURITY_DESCRIPTOR_RELATIVE
- *system.ntfs_attrib* gets/sets ntfs file/dir attributes.
Note: Applied to empty files, this allows to switch type between
sparse(0x200), compressed(0x800) and normal.
Mount Options
=============
The list below describes mount options supported by NTFS3 driver in addition to
generic ones.
generic ones. You can use every mount option with **no** option. If it is in
this table marked with no it means default is without **no**.
===============================================================================
.. flat-table::
:widths: 1 5
:fill-cells:
nls=name This option informs the driver how to interpret path
strings and translate them to Unicode and back. If
this option is not set, the default codepage will be
used (CONFIG_NLS_DEFAULT).
Examples:
'nls=utf8'
* - iocharset=name
- This option informs the driver how to interpret path strings and
translate them to Unicode and back. If this option is not set, the
default codepage will be used (CONFIG_NLS_DEFAULT).
uid=
gid=
umask= Controls the default permissions for files/directories created
after the NTFS volume is mounted.
Example: iocharset=utf8
fmask=
dmask= Instead of specifying umask which applies both to
files and directories, fmask applies only to files and
dmask only to directories.
* - uid=
- :rspan:`1`
* - gid=
nohidden Files with the Windows-specific HIDDEN (FILE_ATTRIBUTE_HIDDEN)
attribute will not be shown under Linux.
* - umask=
- Controls the default permissions for files/directories created after
the NTFS volume is mounted.
sys_immutable Files with the Windows-specific SYSTEM
(FILE_ATTRIBUTE_SYSTEM) attribute will be marked as system
immutable files.
* - dmask=
- :rspan:`1` Instead of specifying umask which applies both to files and
directories, fmask applies only to files and dmask only to directories.
* - fmask=
discard Enable support of the TRIM command for improved performance
on delete operations, which is recommended for use with the
solid-state drives (SSD).
* - noacsrules
- "No access rules" mount option sets access rights for files/folders to
777 and owner/group to root. This mount option absorbs all other
permissions.
force Forces the driver to mount partitions even if 'dirty' flag
(volume dirty) is set. Not recommended for use.
- Permissions change for files/folders will be reported as successful,
but they will remain 777.
sparse Create new files as "sparse".
- Owner/group change will be reported as successful, butthey will stay
as root.
showmeta Use this parameter to show all meta-files (System Files) on
a mounted NTFS partition.
By default, all meta-files are hidden.
* - nohidden
- Files with the Windows-specific HIDDEN (FILE_ATTRIBUTE_HIDDEN) attribute
will not be shown under Linux.
prealloc Preallocate space for files excessively when file size is
increasing on writes. Decreases fragmentation in case of
parallel write operations to different files.
* - sys_immutable
- Files with the Windows-specific SYSTEM (FILE_ATTRIBUTE_SYSTEM) attribute
will be marked as system immutable files.
no_acs_rules "No access rules" mount option sets access rights for
files/folders to 777 and owner/group to root. This mount
option absorbs all other permissions:
- permissions change for files/folders will be reported
as successful, but they will remain 777;
- owner/group change will be reported as successful, but
they will stay as root
* - discard
- Enable support of the TRIM command for improved performance on delete
operations, which is recommended for use with the solid-state drives
(SSD).
acl Support POSIX ACLs (Access Control Lists). Effective if
supported by Kernel. Not to be confused with NTFS ACLs.
The option specified as acl enables support for POSIX ACLs.
* - force
- Forces the driver to mount partitions even if volume is marked dirty.
Not recommended for use.
noatime All files and directories will not update their last access
time attribute if a partition is mounted with this parameter.
This option can speed up file system operation.
* - sparse
- Create new files as sparse.
===============================================================================
* - showmeta
- Use this parameter to show all meta-files (System Files) on a mounted
NTFS partition. By default, all meta-files are hidden.
ToDo list
=========
* - prealloc
- Preallocate space for files excessively when file size is increasing on
writes. Decreases fragmentation in case of parallel write operations to
different files.
- Full journaling support (currently journal replaying is supported) over JBD.
* - acl
- Support POSIX ACLs (Access Control Lists). Effective if supported by
Kernel. Not to be confused with NTFS ACLs. The option specified as acl
enables support for POSIX ACLs.
Todo list
=========
- Full journaling support over JBD. Currently journal replaying is supported
which is not necessarily as effectice as JBD would be.
References
==========
https://www.paragon-software.com/home/ntfs-linux-professional/
- Commercial version of the NTFS driver for Linux.
- Commercial version of the NTFS driver for Linux.
https://www.paragon-software.com/home/ntfs-linux-professional/
almaz.alexandrovich@paragon-software.com
- Direct e-mail address for feedback and requests on the NTFS3 implementation.
- Direct e-mail address for feedback and requests on the NTFS3 implementation.
almaz.alexandrovich@paragon-software.com
......@@ -6,13 +6,9 @@
* TODO: Merge attr_set_size/attr_data_get_block/attr_allocate_frame?
*/
#include <linux/blkdev.h>
#include <linux/buffer_head.h>
#include <linux/fs.h>
#include <linux/hash.h>
#include <linux/nls.h>
#include <linux/ratelimit.h>
#include <linux/slab.h>
#include <linux/kernel.h>
#include "debug.h"
#include "ntfs.h"
......@@ -291,7 +287,7 @@ int attr_make_nonresident(struct ntfs_inode *ni, struct ATTRIB *attr,
if (!rsize) {
/* Empty resident -> Non empty nonresident. */
} else if (!is_data) {
err = ntfs_sb_write_run(sbi, run, 0, data, rsize);
err = ntfs_sb_write_run(sbi, run, 0, data, rsize, 0);
if (err)
goto out2;
} else if (!page) {
......@@ -451,11 +447,8 @@ int attr_set_size(struct ntfs_inode *ni, enum ATTR_TYPE type,
again_1:
align = sbi->cluster_size;
if (is_ext) {
if (is_ext)
align <<= attr_b->nres.c_unit;
if (is_attr_sparsed(attr_b))
keep_prealloc = false;
}
old_valid = le64_to_cpu(attr_b->nres.valid_size);
old_size = le64_to_cpu(attr_b->nres.data_size);
......@@ -465,9 +458,6 @@ int attr_set_size(struct ntfs_inode *ni, enum ATTR_TYPE type,
new_alloc = (new_size + align - 1) & ~(u64)(align - 1);
new_alen = new_alloc >> cluster_bits;
if (keep_prealloc && is_ext)
keep_prealloc = false;
if (keep_prealloc && new_size < old_size) {
attr_b->nres.data_size = cpu_to_le64(new_size);
mi_b->dirty = true;
......@@ -529,7 +519,7 @@ int attr_set_size(struct ntfs_inode *ni, enum ATTR_TYPE type,
} else if (pre_alloc == -1) {
pre_alloc = 0;
if (type == ATTR_DATA && !name_len &&
sbi->options.prealloc) {
sbi->options->prealloc) {
CLST new_alen2 = bytes_to_cluster(
sbi, get_pre_allocated(new_size));
pre_alloc = new_alen2 - new_alen;
......@@ -1966,7 +1956,7 @@ int attr_punch_hole(struct ntfs_inode *ni, u64 vbo, u64 bytes, u32 *frame_size)
return 0;
from = vbo;
to = (vbo + bytes) < data_size ? (vbo + bytes) : data_size;
to = min_t(u64, vbo + bytes, data_size);
memset(Add2Ptr(resident_data(attr_b), from), 0, to - from);
return 0;
}
......
......@@ -5,10 +5,7 @@
*
*/
#include <linux/blkdev.h>
#include <linux/buffer_head.h>
#include <linux/fs.h>
#include <linux/nls.h>
#include "debug.h"
#include "ntfs.h"
......@@ -336,7 +333,7 @@ int al_add_le(struct ntfs_inode *ni, enum ATTR_TYPE type, const __le16 *name,
if (attr && attr->non_res) {
err = ntfs_sb_write_run(ni->mi.sbi, &al->run, 0, al->le,
al->size);
al->size, 0);
if (err)
return err;
al->dirty = false;
......@@ -423,7 +420,7 @@ bool al_delete_le(struct ntfs_inode *ni, enum ATTR_TYPE type, CLST vcn,
return true;
}
int al_update(struct ntfs_inode *ni)
int al_update(struct ntfs_inode *ni, int sync)
{
int err;
struct ATTRIB *attr;
......@@ -445,7 +442,7 @@ int al_update(struct ntfs_inode *ni)
memcpy(resident_data(attr), al->le, al->size);
} else {
err = ntfs_sb_write_run(ni->mi.sbi, &al->run, 0, al->le,
al->size);
al->size, sync);
if (err)
goto out;
......
......@@ -5,13 +5,8 @@
*
*/
#include <linux/blkdev.h>
#include <linux/buffer_head.h>
#include <linux/fs.h>
#include <linux/nls.h>
#include <linux/types.h>
#include "debug.h"
#include "ntfs.h"
#include "ntfs_fs.h"
#define BITS_IN_SIZE_T (sizeof(size_t) * 8)
......@@ -124,8 +119,7 @@ bool are_bits_set(const ulong *lmap, size_t bit, size_t nbits)
pos = nbits & 7;
if (pos) {
u8 mask = fill_mask[pos];
mask = fill_mask[pos];
if ((*map & mask) != mask)
return false;
}
......
......@@ -10,12 +10,10 @@
*
*/
#include <linux/blkdev.h>
#include <linux/buffer_head.h>
#include <linux/fs.h>
#include <linux/nls.h>
#include <linux/kernel.h>
#include "debug.h"
#include "ntfs.h"
#include "ntfs_fs.h"
......@@ -435,7 +433,7 @@ static void wnd_remove_free_ext(struct wnd_bitmap *wnd, size_t bit, size_t len)
;
} else {
n3 = rb_next(&e->count.node);
max_new_len = len > new_len ? len : new_len;
max_new_len = max(len, new_len);
if (!n3) {
wnd->extent_max = max_new_len;
} else {
......@@ -731,7 +729,7 @@ int wnd_set_free(struct wnd_bitmap *wnd, size_t bit, size_t bits)
wbits = wnd->bits_last;
tail = wbits - wbit;
op = tail < bits ? tail : bits;
op = min_t(u32, tail, bits);
bh = wnd_map(wnd, iw);
if (IS_ERR(bh)) {
......@@ -784,7 +782,7 @@ int wnd_set_used(struct wnd_bitmap *wnd, size_t bit, size_t bits)
wbits = wnd->bits_last;
tail = wbits - wbit;
op = tail < bits ? tail : bits;
op = min_t(u32, tail, bits);
bh = wnd_map(wnd, iw);
if (IS_ERR(bh)) {
......@@ -834,7 +832,7 @@ static bool wnd_is_free_hlp(struct wnd_bitmap *wnd, size_t bit, size_t bits)
wbits = wnd->bits_last;
tail = wbits - wbit;
op = tail < bits ? tail : bits;
op = min_t(u32, tail, bits);
if (wbits != wnd->free_bits[iw]) {
bool ret;
......@@ -926,7 +924,7 @@ bool wnd_is_used(struct wnd_bitmap *wnd, size_t bit, size_t bits)
wbits = wnd->bits_last;
tail = wbits - wbit;
op = tail < bits ? tail : bits;
op = min_t(u32, tail, bits);
if (wnd->free_bits[iw]) {
bool ret;
......
......@@ -11,6 +11,9 @@
#ifndef _LINUX_NTFS3_DEBUG_H
#define _LINUX_NTFS3_DEBUG_H
struct super_block;
struct inode;
#ifndef Add2Ptr
#define Add2Ptr(P, I) ((void *)((u8 *)(P) + (I)))
#define PtrOffset(B, O) ((size_t)((size_t)(O) - (size_t)(B)))
......
......@@ -7,10 +7,7 @@
*
*/
#include <linux/blkdev.h>
#include <linux/buffer_head.h>
#include <linux/fs.h>
#include <linux/iversion.h>
#include <linux/nls.h>
#include "debug.h"
......@@ -18,30 +15,27 @@
#include "ntfs_fs.h"
/* Convert little endian UTF-16 to NLS string. */
int ntfs_utf16_to_nls(struct ntfs_sb_info *sbi, const struct le_str *uni,
int ntfs_utf16_to_nls(struct ntfs_sb_info *sbi, const __le16 *name, u32 len,
u8 *buf, int buf_len)
{
int ret, uni_len, warn;
const __le16 *ip;
int ret, warn;
u8 *op;
struct nls_table *nls = sbi->options.nls;
struct nls_table *nls = sbi->options->nls;
static_assert(sizeof(wchar_t) == sizeof(__le16));
if (!nls) {
/* UTF-16 -> UTF-8 */
ret = utf16s_to_utf8s((wchar_t *)uni->name, uni->len,
UTF16_LITTLE_ENDIAN, buf, buf_len);
ret = utf16s_to_utf8s(name, len, UTF16_LITTLE_ENDIAN, buf,
buf_len);
buf[ret] = '\0';
return ret;
}
ip = uni->name;
op = buf;
uni_len = uni->len;
warn = 0;
while (uni_len--) {
while (len--) {
u16 ec;
int charlen;
char dump[5];
......@@ -52,7 +46,7 @@ int ntfs_utf16_to_nls(struct ntfs_sb_info *sbi, const struct le_str *uni,
break;
}
ec = le16_to_cpu(*ip++);
ec = le16_to_cpu(*name++);
charlen = nls->uni2char(ec, op, buf_len);
if (charlen > 0) {
......@@ -186,7 +180,7 @@ int ntfs_nls_to_utf16(struct ntfs_sb_info *sbi, const u8 *name, u32 name_len,
{
int ret, slen;
const u8 *end;
struct nls_table *nls = sbi->options.nls;
struct nls_table *nls = sbi->options->nls;
u16 *uname = uni->name;
static_assert(sizeof(wchar_t) == sizeof(u16));
......@@ -301,14 +295,14 @@ static inline int ntfs_filldir(struct ntfs_sb_info *sbi, struct ntfs_inode *ni,
return 0;
/* Skip meta files. Unless option to show metafiles is set. */
if (!sbi->options.showmeta && ntfs_is_meta_file(sbi, ino))
if (!sbi->options->showmeta && ntfs_is_meta_file(sbi, ino))
return 0;
if (sbi->options.nohidden && (fname->dup.fa & FILE_ATTRIBUTE_HIDDEN))
if (sbi->options->nohidden && (fname->dup.fa & FILE_ATTRIBUTE_HIDDEN))
return 0;
name_len = ntfs_utf16_to_nls(sbi, (struct le_str *)&fname->name_len,
name, PATH_MAX);
name_len = ntfs_utf16_to_nls(sbi, fname->name, fname->name_len, name,
PATH_MAX);
if (name_len <= 0) {
ntfs_warn(sbi->sb, "failed to convert name for inode %lx.",
ino);
......
......@@ -12,7 +12,6 @@
#include <linux/compat.h>
#include <linux/falloc.h>
#include <linux/fiemap.h>
#include <linux/nls.h>
#include "debug.h"
#include "ntfs.h"
......@@ -588,8 +587,11 @@ static long ntfs_fallocate(struct file *file, int mode, loff_t vbo, loff_t len)
truncate_pagecache(inode, vbo_down);
if (!is_sparsed(ni) && !is_compressed(ni)) {
/* Normal file. */
err = ntfs_zero_range(inode, vbo, end);
/*
* Normal file, can't make hole.
* TODO: Try to find way to save info about hole.
*/
err = -EOPNOTSUPP;
goto out;
}
......@@ -737,7 +739,7 @@ int ntfs3_setattr(struct user_namespace *mnt_userns, struct dentry *dentry,
umode_t mode = inode->i_mode;
int err;
if (sbi->options.no_acs_rules) {
if (sbi->options->noacsrules) {
/* "No access rules" - Force any changes of time etc. */
attr->ia_valid |= ATTR_FORCE;
/* and disable for editing some attributes. */
......@@ -1185,7 +1187,7 @@ static int ntfs_file_release(struct inode *inode, struct file *file)
int err = 0;
/* If we are last writer on the inode, drop the block reservation. */
if (sbi->options.prealloc && ((file->f_mode & FMODE_WRITE) &&
if (sbi->options->prealloc && ((file->f_mode & FMODE_WRITE) &&
atomic_read(&inode->i_writecount) == 1)) {
ni_lock(ni);
down_write(&ni->file.run_lock);
......
......@@ -5,11 +5,8 @@
*
*/
#include <linux/blkdev.h>
#include <linux/buffer_head.h>
#include <linux/fiemap.h>
#include <linux/fs.h>
#include <linux/nls.h>
#include <linux/vmalloc.h>
#include "debug.h"
......@@ -708,18 +705,35 @@ static int ni_try_remove_attr_list(struct ntfs_inode *ni)
continue;
mi = ni_find_mi(ni, ino_get(&le->ref));
if (!mi) {
/* Should never happened, 'cause already checked. */
goto bad;
}
attr = mi_find_attr(mi, NULL, le->type, le_name(le),
le->name_len, &le->id);
if (!attr) {
/* Should never happened, 'cause already checked. */
goto bad;
}
asize = le32_to_cpu(attr->size);
/* Insert into primary record. */
attr_ins = mi_insert_attr(&ni->mi, le->type, le_name(le),
le->name_len, asize,
le16_to_cpu(attr->name_off));
id = attr_ins->id;
if (!attr_ins) {
/*
* Internal error.
* Either no space in primary record (already checked).
* Either tried to insert another
* non indexed attribute (logic error).
*/
goto bad;
}
/* Copy all except id. */
id = attr_ins->id;
memcpy(attr_ins, attr, asize);
attr_ins->id = id;
......@@ -735,6 +749,10 @@ static int ni_try_remove_attr_list(struct ntfs_inode *ni)
ni->attr_list.dirty = false;
return 0;
bad:
ntfs_inode_err(&ni->vfs_inode, "Internal error");
make_bad_inode(&ni->vfs_inode);
return -EINVAL;
}
/*
......@@ -956,6 +974,13 @@ static int ni_ins_attr_ext(struct ntfs_inode *ni, struct ATTR_LIST_ENTRY *le,
continue;
}
/*
* Do not try to insert this attribute
* if there is no room in record.
*/
if (le32_to_cpu(mi->mrec->used) + asize > sbi->record_size)
continue;
/* Try to insert attribute into this subrecord. */
attr = ni_ins_new_attr(ni, mi, le, type, name, name_len, asize,
name_off, svcn, ins_le);
......@@ -1451,7 +1476,7 @@ int ni_insert_resident(struct ntfs_inode *ni, u32 data_size,
attr->res.flags = RESIDENT_FLAG_INDEXED;
/* is_attr_indexed(attr)) == true */
le16_add_cpu(&ni->mi.mrec->hard_links, +1);
le16_add_cpu(&ni->mi.mrec->hard_links, 1);
ni->mi.dirty = true;
}
attr->res.res = 0;
......@@ -1606,7 +1631,7 @@ struct ATTR_FILE_NAME *ni_fname_type(struct ntfs_inode *ni, u8 name_type,
*le = NULL;
if (FILE_NAME_POSIX == name_type)
if (name_type == FILE_NAME_POSIX)
return NULL;
/* Enumerate all names. */
......@@ -1706,18 +1731,16 @@ int ni_new_attr_flags(struct ntfs_inode *ni, enum FILE_ATTRIBUTE new_fa)
/*
* ni_parse_reparse
*
* Buffer is at least 24 bytes.
* buffer - memory for reparse buffer header
*/
enum REPARSE_SIGN ni_parse_reparse(struct ntfs_inode *ni, struct ATTRIB *attr,
void *buffer)
struct REPARSE_DATA_BUFFER *buffer)
{
const struct REPARSE_DATA_BUFFER *rp = NULL;
u8 bits;
u16 len;
typeof(rp->CompressReparseBuffer) *cmpr;
static_assert(sizeof(struct REPARSE_DATA_BUFFER) <= 24);
/* Try to estimate reparse point. */
if (!attr->non_res) {
rp = resident_data_ex(attr, sizeof(struct REPARSE_DATA_BUFFER));
......@@ -1803,6 +1826,9 @@ enum REPARSE_SIGN ni_parse_reparse(struct ntfs_inode *ni, struct ATTRIB *attr,
return REPARSE_NONE;
}
if (buffer != rp)
memcpy(buffer, rp, sizeof(struct REPARSE_DATA_BUFFER));
/* Looks like normal symlink. */
return REPARSE_LINK;
}
......@@ -2906,9 +2932,8 @@ bool ni_remove_name_undo(struct ntfs_inode *dir_ni, struct ntfs_inode *ni,
memcpy(Add2Ptr(attr, SIZEOF_RESIDENT), de + 1, de_key_size);
mi_get_ref(&ni->mi, &de->ref);
if (indx_insert_entry(&dir_ni->dir, dir_ni, de, sbi, NULL, 1)) {
if (indx_insert_entry(&dir_ni->dir, dir_ni, de, sbi, NULL, 1))
return false;
}
}
return true;
......@@ -3077,7 +3102,9 @@ static bool ni_update_parent(struct ntfs_inode *ni, struct NTFS_DUP_INFO *dup,
const struct EA_INFO *info;
info = resident_data_ex(attr, sizeof(struct EA_INFO));
dup->ea_size = info->size_pack;
/* If ATTR_EA_INFO exists 'info' can't be NULL. */
if (info)
dup->ea_size = info->size_pack;
}
}
......@@ -3205,7 +3232,7 @@ int ni_write_inode(struct inode *inode, int sync, const char *hint)
goto out;
}
err = al_update(ni);
err = al_update(ni, sync);
if (err)
goto out;
}
......
......@@ -6,12 +6,8 @@
*/
#include <linux/blkdev.h>
#include <linux/buffer_head.h>
#include <linux/fs.h>
#include <linux/hash.h>
#include <linux/nls.h>
#include <linux/random.h>
#include <linux/ratelimit.h>
#include <linux/slab.h>
#include "debug.h"
......@@ -2219,7 +2215,7 @@ static int last_log_lsn(struct ntfs_log *log)
err = ntfs_sb_write_run(log->ni->mi.sbi,
&log->ni->file.run, off, page,
log->page_size);
log->page_size, 0);
if (err)
goto out;
......@@ -3710,7 +3706,7 @@ static int do_action(struct ntfs_log *log, struct OPEN_ATTR_ENRTY *oe,
if (a_dirty) {
attr = oa->attr;
err = ntfs_sb_write_run(sbi, oa->run1, vbo, buffer_le, bytes);
err = ntfs_sb_write_run(sbi, oa->run1, vbo, buffer_le, bytes, 0);
if (err)
goto out;
}
......@@ -5152,10 +5148,10 @@ int log_replay(struct ntfs_inode *ni, bool *initialized)
ntfs_fix_pre_write(&rh->rhdr, log->page_size);
err = ntfs_sb_write_run(sbi, &ni->file.run, 0, rh, log->page_size);
err = ntfs_sb_write_run(sbi, &ni->file.run, 0, rh, log->page_size, 0);
if (!err)
err = ntfs_sb_write_run(sbi, &log->ni->file.run, log->page_size,
rh, log->page_size);
rh, log->page_size, 0);
kfree(rh);
if (err)
......
......@@ -8,7 +8,7 @@
#include <linux/blkdev.h>
#include <linux/buffer_head.h>
#include <linux/fs.h>
#include <linux/nls.h>
#include <linux/kernel.h>
#include "debug.h"
#include "ntfs.h"
......@@ -358,7 +358,7 @@ int ntfs_look_for_free_space(struct ntfs_sb_info *sbi, CLST lcn, CLST len,
enum ALLOCATE_OPT opt)
{
int err;
CLST alen = 0;
CLST alen;
struct super_block *sb = sbi->sb;
size_t alcn, zlen, zeroes, zlcn, zlen2, ztrim, new_zlen;
struct wnd_bitmap *wnd = &sbi->used.bitmap;
......@@ -370,27 +370,28 @@ int ntfs_look_for_free_space(struct ntfs_sb_info *sbi, CLST lcn, CLST len,
if (!zlen) {
err = ntfs_refresh_zone(sbi);
if (err)
goto out;
goto up_write;
zlen = wnd_zone_len(wnd);
}
if (!zlen) {
ntfs_err(sbi->sb, "no free space to extend mft");
goto out;
err = -ENOSPC;
goto up_write;
}
lcn = wnd_zone_bit(wnd);
alen = zlen > len ? len : zlen;
alen = min_t(CLST, len, zlen);
wnd_zone_set(wnd, lcn + alen, zlen - alen);
err = wnd_set_used(wnd, lcn, alen);
if (err) {
up_write(&wnd->rw_lock);
return err;
}
if (err)
goto up_write;
alcn = lcn;
goto out;
goto space_found;
}
/*
* 'Cause cluster 0 is always used this value means that we should use
......@@ -404,49 +405,45 @@ int ntfs_look_for_free_space(struct ntfs_sb_info *sbi, CLST lcn, CLST len,
alen = wnd_find(wnd, len, lcn, BITMAP_FIND_MARK_AS_USED, &alcn);
if (alen)
goto out;
goto space_found;
/* Try to use clusters from MftZone. */
zlen = wnd_zone_len(wnd);
zeroes = wnd_zeroes(wnd);
/* Check too big request */
if (len > zeroes + zlen || zlen <= NTFS_MIN_MFT_ZONE)
goto out;
if (len > zeroes + zlen || zlen <= NTFS_MIN_MFT_ZONE) {
err = -ENOSPC;
goto up_write;
}
/* How many clusters to cat from zone. */
zlcn = wnd_zone_bit(wnd);
zlen2 = zlen >> 1;
ztrim = len > zlen ? zlen : (len > zlen2 ? len : zlen2);
new_zlen = zlen - ztrim;
if (new_zlen < NTFS_MIN_MFT_ZONE) {
new_zlen = NTFS_MIN_MFT_ZONE;
if (new_zlen > zlen)
new_zlen = zlen;
}
ztrim = clamp_val(len, zlen2, zlen);
new_zlen = max_t(size_t, zlen - ztrim, NTFS_MIN_MFT_ZONE);
wnd_zone_set(wnd, zlcn, new_zlen);
/* Allocate continues clusters. */
alen = wnd_find(wnd, len, 0,
BITMAP_FIND_MARK_AS_USED | BITMAP_FIND_FULL, &alcn);
out:
if (alen) {
err = 0;
*new_len = alen;
*new_lcn = alcn;
ntfs_unmap_meta(sb, alcn, alen);
/* Set hint for next requests. */
if (!(opt & ALLOCATE_MFT))
sbi->used.next_free_lcn = alcn + alen;
} else {
if (!alen) {
err = -ENOSPC;
goto up_write;
}
space_found:
err = 0;
*new_len = alen;
*new_lcn = alcn;
ntfs_unmap_meta(sb, alcn, alen);
/* Set hint for next requests. */
if (!(opt & ALLOCATE_MFT))
sbi->used.next_free_lcn = alcn + alen;
up_write:
up_write(&wnd->rw_lock);
return err;
}
......@@ -1080,7 +1077,7 @@ int ntfs_sb_write(struct super_block *sb, u64 lbo, size_t bytes,
}
int ntfs_sb_write_run(struct ntfs_sb_info *sbi, const struct runs_tree *run,
u64 vbo, const void *buf, size_t bytes)
u64 vbo, const void *buf, size_t bytes, int sync)
{
struct super_block *sb = sbi->sb;
u8 cluster_bits = sbi->cluster_bits;
......@@ -1099,8 +1096,8 @@ int ntfs_sb_write_run(struct ntfs_sb_info *sbi, const struct runs_tree *run,
len = ((u64)clen << cluster_bits) - off;
for (;;) {
u32 op = len < bytes ? len : bytes;
int err = ntfs_sb_write(sb, lbo, op, buf, 0);
u32 op = min_t(u64, len, bytes);
int err = ntfs_sb_write(sb, lbo, op, buf, sync);
if (err)
return err;
......@@ -1300,7 +1297,7 @@ int ntfs_get_bh(struct ntfs_sb_info *sbi, const struct runs_tree *run, u64 vbo,
nb->off = off = lbo & (blocksize - 1);
for (;;) {
u32 len32 = len < bytes ? len : bytes;
u32 len32 = min_t(u64, len, bytes);
sector_t block = lbo >> sb->s_blocksize_bits;
do {
......@@ -2175,7 +2172,7 @@ int ntfs_insert_security(struct ntfs_sb_info *sbi,
/* Write main SDS bucket. */
err = ntfs_sb_write_run(sbi, &ni->file.run, sbi->security.next_off,
d_security, aligned_sec_size);
d_security, aligned_sec_size, 0);
if (err)
goto out;
......@@ -2193,7 +2190,7 @@ int ntfs_insert_security(struct ntfs_sb_info *sbi,
/* Write copy SDS bucket. */
err = ntfs_sb_write_run(sbi, &ni->file.run, mirr_off, d_security,
aligned_sec_size);
aligned_sec_size, 0);
if (err)
goto out;
......
......@@ -8,7 +8,7 @@
#include <linux/blkdev.h>
#include <linux/buffer_head.h>
#include <linux/fs.h>
#include <linux/nls.h>
#include <linux/kernel.h>
#include "debug.h"
#include "ntfs.h"
......@@ -671,138 +671,74 @@ static struct NTFS_DE *hdr_find_e(const struct ntfs_index *indx,
const struct INDEX_HDR *hdr, const void *key,
size_t key_len, const void *ctx, int *diff)
{
struct NTFS_DE *e;
struct NTFS_DE *e, *found = NULL;
NTFS_CMP_FUNC cmp = indx->cmp;
int min_idx = 0, mid_idx, max_idx = 0;
int diff2;
int table_size = 8;
u32 e_size, e_key_len;
u32 end = le32_to_cpu(hdr->used);
u32 off = le32_to_cpu(hdr->de_off);
u16 offs[128];
#ifdef NTFS3_INDEX_BINARY_SEARCH
int max_idx = 0, fnd, min_idx;
int nslots = 64;
u16 *offs;
if (end > 0x10000)
goto next;
offs = kmalloc(sizeof(u16) * nslots, GFP_NOFS);
if (!offs)
goto next;
fill_table:
if (off + sizeof(struct NTFS_DE) > end)
return NULL;
/* Use binary search algorithm. */
next1:
if (off + sizeof(struct NTFS_DE) > end) {
e = NULL;
goto out1;
}
e = Add2Ptr(hdr, off);
e_size = le16_to_cpu(e->size);
if (e_size < sizeof(struct NTFS_DE) || off + e_size > end) {
e = NULL;
goto out1;
}
if (max_idx >= nslots) {
u16 *ptr;
int new_slots = ALIGN(2 * nslots, 8);
ptr = kmalloc(sizeof(u16) * new_slots, GFP_NOFS);
if (ptr)
memcpy(ptr, offs, sizeof(u16) * max_idx);
kfree(offs);
offs = ptr;
nslots = new_slots;
if (!ptr)
goto next;
}
/* Store entry table. */
offs[max_idx] = off;
if (e_size < sizeof(struct NTFS_DE) || off + e_size > end)
return NULL;
if (!de_is_last(e)) {
offs[max_idx] = off;
off += e_size;
max_idx += 1;
goto next1;
}
/*
* Table of pointers is created.
* Use binary search to find entry that is <= to the search value.
*/
fnd = -1;
min_idx = 0;
max_idx++;
if (max_idx < table_size)
goto fill_table;
while (min_idx <= max_idx) {
int mid_idx = min_idx + ((max_idx - min_idx) >> 1);
int diff2;
e = Add2Ptr(hdr, offs[mid_idx]);
max_idx--;
}
e_key_len = le16_to_cpu(e->key_size);
binary_search:
e_key_len = le16_to_cpu(e->key_size);
diff2 = (*cmp)(key, key_len, e + 1, e_key_len, ctx);
diff2 = (*cmp)(key, key_len, e + 1, e_key_len, ctx);
if (diff2 > 0) {
if (found) {
min_idx = mid_idx + 1;
} else {
if (de_is_last(e))
return NULL;
if (!diff2) {
*diff = 0;
goto out1;
max_idx = 0;
table_size = min(table_size * 2,
(int)ARRAY_SIZE(offs));
goto fill_table;
}
if (diff2 < 0) {
} else if (diff2 < 0) {
if (found)
max_idx = mid_idx - 1;
fnd = mid_idx;
if (!fnd)
break;
} else {
min_idx = mid_idx + 1;
}
}
else
max_idx--;
if (fnd == -1) {
e = NULL;
goto out1;
found = e;
} else {
*diff = 0;
return e;
}
*diff = -1;
e = Add2Ptr(hdr, offs[fnd]);
out1:
kfree(offs);
return e;
#endif
next:
/*
* Entries index are sorted.
* Enumerate all entries until we find entry
* that is <= to the search value.
*/
if (off + sizeof(struct NTFS_DE) > end)
return NULL;
e = Add2Ptr(hdr, off);
e_size = le16_to_cpu(e->size);
if (e_size < sizeof(struct NTFS_DE) || off + e_size > end)
return NULL;
off += e_size;
e_key_len = le16_to_cpu(e->key_size);
*diff = (*cmp)(key, key_len, e + 1, e_key_len, ctx);
if (!*diff)
return e;
if (min_idx > max_idx) {
*diff = -1;
return found;
}
if (*diff <= 0)
return e;
mid_idx = (min_idx + max_idx) >> 1;
e = Add2Ptr(hdr, offs[mid_idx]);
if (de_is_last(e)) {
*diff = 1;
return e;
}
goto next;
goto binary_search;
}
/*
......@@ -1136,9 +1072,7 @@ int indx_find(struct ntfs_index *indx, struct ntfs_inode *ni,
if (!e)
return -EINVAL;
if (fnd)
fnd->root_de = e;
fnd->root_de = e;
err = 0;
for (;;) {
......@@ -1401,7 +1335,7 @@ int indx_find_raw(struct ntfs_index *indx, struct ntfs_inode *ni,
static int indx_create_allocate(struct ntfs_index *indx, struct ntfs_inode *ni,
CLST *vbn)
{
int err = -ENOMEM;
int err;
struct ntfs_sb_info *sbi = ni->mi.sbi;
struct ATTRIB *bitmap;
struct ATTRIB *alloc;
......
This diff is collapsed.
......@@ -5,6 +5,9 @@
* Copyright (C) 2015 Eric Biggers
*/
#ifndef _LINUX_NTFS3_LIB_DECOMPRESS_COMMON_H
#define _LINUX_NTFS3_LIB_DECOMPRESS_COMMON_H
#include <linux/string.h>
#include <linux/compiler.h>
#include <linux/types.h>
......@@ -336,3 +339,5 @@ static forceinline u8 *lz_copy(u8 *dst, u32 length, u32 offset, const u8 *bufend
return dst;
}
#endif /* _LINUX_NTFS3_LIB_DECOMPRESS_COMMON_H */
......@@ -7,6 +7,10 @@
* - linux kernel code style
*/
#ifndef _LINUX_NTFS3_LIB_LIB_H
#define _LINUX_NTFS3_LIB_LIB_H
#include <linux/types.h>
/* globals from xpress_decompress.c */
struct xpress_decompressor *xpress_allocate_decompressor(void);
......@@ -24,3 +28,5 @@ int lzx_decompress(struct lzx_decompressor *__restrict d,
const void *__restrict compressed_data,
size_t compressed_size, void *__restrict uncompressed_data,
size_t uncompressed_size);
#endif /* _LINUX_NTFS3_LIB_LIB_H */
......@@ -5,13 +5,13 @@
*
*/
#include <linux/blkdev.h>
#include <linux/buffer_head.h>
#include <linux/fs.h>
#include <linux/nls.h>
#include <linux/kernel.h>
#include <linux/slab.h>
#include <linux/stddef.h>
#include <linux/string.h>
#include <linux/types.h>
#include "debug.h"
#include "ntfs.h"
#include "ntfs_fs.h"
// clang-format off
......@@ -292,7 +292,7 @@ static inline ssize_t decompress_chunk(u8 *unc, u8 *unc_end, const u8 *cmpr,
/*
* get_lznt_ctx
* @level: 0 - Standard compression.
* !0 - Best compression, requires a lot of cpu.
* !0 - Best compression, requires a lot of cpu.
*/
struct lznt *get_lznt_ctx(int level)
{
......
......@@ -5,11 +5,7 @@
*
*/
#include <linux/blkdev.h>
#include <linux/buffer_head.h>
#include <linux/fs.h>
#include <linux/iversion.h>
#include <linux/namei.h>
#include <linux/nls.h>
#include "debug.h"
......@@ -99,16 +95,11 @@ static struct dentry *ntfs_lookup(struct inode *dir, struct dentry *dentry,
static int ntfs_create(struct user_namespace *mnt_userns, struct inode *dir,
struct dentry *dentry, umode_t mode, bool excl)
{
struct ntfs_inode *ni = ntfs_i(dir);
struct inode *inode;
ni_lock_dir(ni);
inode = ntfs_create_inode(mnt_userns, dir, dentry, NULL, S_IFREG | mode,
0, NULL, 0, NULL);
ni_unlock(ni);
return IS_ERR(inode) ? PTR_ERR(inode) : 0;
}
......@@ -120,16 +111,11 @@ static int ntfs_create(struct user_namespace *mnt_userns, struct inode *dir,
static int ntfs_mknod(struct user_namespace *mnt_userns, struct inode *dir,
struct dentry *dentry, umode_t mode, dev_t rdev)
{
struct ntfs_inode *ni = ntfs_i(dir);
struct inode *inode;
ni_lock_dir(ni);
inode = ntfs_create_inode(mnt_userns, dir, dentry, NULL, mode, rdev,
NULL, 0, NULL);
ni_unlock(ni);
return IS_ERR(inode) ? PTR_ERR(inode) : 0;
}
......@@ -200,15 +186,10 @@ static int ntfs_symlink(struct user_namespace *mnt_userns, struct inode *dir,
{
u32 size = strlen(symname);
struct inode *inode;
struct ntfs_inode *ni = ntfs_i(dir);
ni_lock_dir(ni);
inode = ntfs_create_inode(mnt_userns, dir, dentry, NULL, S_IFLNK | 0777,
0, symname, size, NULL);
ni_unlock(ni);
return IS_ERR(inode) ? PTR_ERR(inode) : 0;
}
......@@ -219,15 +200,10 @@ static int ntfs_mkdir(struct user_namespace *mnt_userns, struct inode *dir,
struct dentry *dentry, umode_t mode)
{
struct inode *inode;
struct ntfs_inode *ni = ntfs_i(dir);
ni_lock_dir(ni);
inode = ntfs_create_inode(mnt_userns, dir, dentry, NULL, S_IFDIR | mode,
0, NULL, 0, NULL);
ni_unlock(ni);
return IS_ERR(inode) ? PTR_ERR(inode) : 0;
}
......
......@@ -10,19 +10,27 @@
#ifndef _LINUX_NTFS3_NTFS_H
#define _LINUX_NTFS3_NTFS_H
/* TODO: Check 4K MFT record and 512 bytes cluster. */
#include <linux/blkdev.h>
#include <linux/build_bug.h>
#include <linux/kernel.h>
#include <linux/stddef.h>
#include <linux/string.h>
#include <linux/types.h>
#include "debug.h"
/* Activate this define to use binary search in indexes. */
#define NTFS3_INDEX_BINARY_SEARCH
/* TODO: Check 4K MFT record and 512 bytes cluster. */
/* Check each run for marked clusters. */
#define NTFS3_CHECK_FREE_CLST
#define NTFS_NAME_LEN 255
/* ntfs.sys used 500 maximum links on-disk struct allows up to 0xffff. */
#define NTFS_LINK_MAX 0x400
//#define NTFS_LINK_MAX 0xffff
/*
* ntfs.sys used 500 maximum links on-disk struct allows up to 0xffff.
* xfstest generic/041 creates 3003 hardlinks.
*/
#define NTFS_LINK_MAX 4000
/*
* Activate to use 64 bit clusters instead of 32 bits in ntfs.sys.
......
......@@ -9,6 +9,37 @@
#ifndef _LINUX_NTFS3_NTFS_FS_H
#define _LINUX_NTFS3_NTFS_FS_H
#include <linux/blkdev.h>
#include <linux/buffer_head.h>
#include <linux/cleancache.h>
#include <linux/fs.h>
#include <linux/highmem.h>
#include <linux/kernel.h>
#include <linux/mm.h>
#include <linux/mutex.h>
#include <linux/page-flags.h>
#include <linux/pagemap.h>
#include <linux/rbtree.h>
#include <linux/rwsem.h>
#include <linux/slab.h>
#include <linux/string.h>
#include <linux/time64.h>
#include <linux/types.h>
#include <linux/uidgid.h>
#include <asm/div64.h>
#include <asm/page.h>
#include "debug.h"
#include "ntfs.h"
struct dentry;
struct fiemap_extent_info;
struct user_namespace;
struct page;
struct writeback_control;
enum utf16_endian;
#define MINUS_ONE_T ((size_t)(-1))
/* Biggest MFT / smallest cluster */
#define MAXIMUM_BYTES_PER_MFT 4096
......@@ -52,6 +83,7 @@
// clang-format on
struct ntfs_mount_options {
char *nls_name;
struct nls_table *nls;
kuid_t fs_uid;
......@@ -59,19 +91,16 @@ struct ntfs_mount_options {
u16 fs_fmask_inv;
u16 fs_dmask_inv;
unsigned uid : 1, /* uid was set. */
gid : 1, /* gid was set. */
fmask : 1, /* fmask was set. */
dmask : 1, /* dmask was set. */
sys_immutable : 1, /* Immutable system files. */
discard : 1, /* Issue discard requests on deletions. */
sparse : 1, /* Create sparse files. */
showmeta : 1, /* Show meta files. */
nohidden : 1, /* Do not show hidden files. */
force : 1, /* Rw mount dirty volume. */
no_acs_rules : 1, /*Exclude acs rules. */
prealloc : 1 /* Preallocate space when file is growing. */
;
unsigned fmask : 1; /* fmask was set. */
unsigned dmask : 1; /*dmask was set. */
unsigned sys_immutable : 1; /* Immutable system files. */
unsigned discard : 1; /* Issue discard requests on deletions. */
unsigned sparse : 1; /* Create sparse files. */
unsigned showmeta : 1; /* Show meta files. */
unsigned nohidden : 1; /* Do not show hidden files. */
unsigned force : 1; /* RW mount dirty volume. */
unsigned noacsrules : 1; /* Exclude acs rules. */
unsigned prealloc : 1; /* Preallocate space when file is growing. */
};
/* Special value to unpack and deallocate. */
......@@ -182,10 +211,8 @@ struct ntfs_sb_info {
u32 blocks_per_cluster; // cluster_size / sb->s_blocksize
u32 record_size;
u32 sector_size;
u32 index_size;
u8 sector_bits;
u8 cluster_bits;
u8 record_bits;
......@@ -279,7 +306,7 @@ struct ntfs_sb_info {
#endif
} compress;
struct ntfs_mount_options options;
struct ntfs_mount_options *options;
struct ratelimit_state msg_ratelimit;
};
......@@ -436,7 +463,7 @@ bool al_remove_le(struct ntfs_inode *ni, struct ATTR_LIST_ENTRY *le);
bool al_delete_le(struct ntfs_inode *ni, enum ATTR_TYPE type, CLST vcn,
const __le16 *name, size_t name_len,
const struct MFT_REF *ref);
int al_update(struct ntfs_inode *ni);
int al_update(struct ntfs_inode *ni, int sync);
static inline size_t al_aligned(size_t size)
{
return (size + 1023) & ~(size_t)1023;
......@@ -448,7 +475,7 @@ bool are_bits_set(const ulong *map, size_t bit, size_t nbits);
size_t get_set_bits_ex(const ulong *map, size_t bit, size_t nbits);
/* Globals from dir.c */
int ntfs_utf16_to_nls(struct ntfs_sb_info *sbi, const struct le_str *uni,
int ntfs_utf16_to_nls(struct ntfs_sb_info *sbi, const __le16 *name, u32 len,
u8 *buf, int buf_len);
int ntfs_nls_to_utf16(struct ntfs_sb_info *sbi, const u8 *name, u32 name_len,
struct cpu_str *uni, u32 max_ulen,
......@@ -520,7 +547,7 @@ struct ATTR_FILE_NAME *ni_fname_type(struct ntfs_inode *ni, u8 name_type,
struct ATTR_LIST_ENTRY **entry);
int ni_new_attr_flags(struct ntfs_inode *ni, enum FILE_ATTRIBUTE new_fa);
enum REPARSE_SIGN ni_parse_reparse(struct ntfs_inode *ni, struct ATTRIB *attr,
void *buffer);
struct REPARSE_DATA_BUFFER *buffer);
int ni_write_inode(struct inode *inode, int sync, const char *hint);
#define _ni_write_inode(i, w) ni_write_inode(i, w, __func__)
int ni_fiemap(struct ntfs_inode *ni, struct fiemap_extent_info *fieinfo,
......@@ -577,7 +604,7 @@ int ntfs_sb_read(struct super_block *sb, u64 lbo, size_t bytes, void *buffer);
int ntfs_sb_write(struct super_block *sb, u64 lbo, size_t bytes,
const void *buffer, int wait);
int ntfs_sb_write_run(struct ntfs_sb_info *sbi, const struct runs_tree *run,
u64 vbo, const void *buf, size_t bytes);
u64 vbo, const void *buf, size_t bytes, int sync);
struct buffer_head *ntfs_bread_run(struct ntfs_sb_info *sbi,
const struct runs_tree *run, u64 vbo);
int ntfs_read_run_nb(struct ntfs_sb_info *sbi, const struct runs_tree *run,
......
......@@ -5,10 +5,7 @@
*
*/
#include <linux/blkdev.h>
#include <linux/buffer_head.h>
#include <linux/fs.h>
#include <linux/nls.h>
#include "debug.h"
#include "ntfs.h"
......
......@@ -7,10 +7,8 @@
*/
#include <linux/blkdev.h>
#include <linux/buffer_head.h>
#include <linux/fs.h>
#include <linux/log2.h>
#include <linux/nls.h>
#include "debug.h"
#include "ntfs.h"
......
This diff is collapsed.
......@@ -5,13 +5,9 @@
*
*/
#include <linux/blkdev.h>
#include <linux/buffer_head.h>
#include <linux/module.h>
#include <linux/nls.h>
#include <linux/kernel.h>
#include <linux/types.h>
#include "debug.h"
#include "ntfs.h"
#include "ntfs_fs.h"
static inline u16 upcase_unicode_char(const u16 *upcase, u16 chr)
......
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment