Commit 2e4564b3 authored by Steve French's avatar Steve French

smb3: add support for stat of WSL reparse points for special file types

This is needed so when mounting to Windows we do not
misinterpret various special files created by Linux (WSL) as symlinks.
An earlier patch addressed readdir.  This patch fixes stat (getattr).

With this patch:
  File: /mnt1/char
  Size: 0          Blocks: 0          IO Block: 16384  character special file
Device: 34h/52d Inode: 844424930132069  Links: 1     Device type: 0,0
Access: (0755/crwxr-xr-x)  Uid: (    0/    root)   Gid: (    0/    root)
Access: 2020-10-21 17:46:51.839458900 -0500
Modify: 2020-10-21 17:46:51.839458900 -0500
Change: 2020-10-21 18:30:39.797358800 -0500
 Birth: -
  File: /mnt1/fifo
  Size: 0          Blocks: 0          IO Block: 16384  fifo
Device: 34h/52d Inode: 1125899906842722  Links: 1
Access: (0755/prwxr-xr-x)  Uid: (    0/    root)   Gid: (    0/    root)
Access: 2020-10-21 16:21:37.259249700 -0500
Modify: 2020-10-21 16:21:37.259249700 -0500
Change: 2020-10-21 18:30:39.797358800 -0500
 Birth: -
  File: /mnt1/block
  Size: 0          Blocks: 0          IO Block: 16384  block special file
Device: 34h/52d Inode: 844424930132068  Links: 1     Device type: 0,0
Access: (0755/brwxr-xr-x)  Uid: (    0/    root)   Gid: (    0/    root)
Access: 2020-10-21 17:10:47.913103200 -0500
Modify: 2020-10-21 17:10:47.913103200 -0500
Change: 2020-10-21 18:30:39.796725500 -0500
 Birth: -

without the patch all show up incorrectly as symlinks with annoying "operation not supported error also returned"
  File: /mnt1/charstat: cannot read symbolic link '/mnt1/char': Operation not supported

  Size: 0          Blocks: 0          IO Block: 16384  symbolic link
Device: 34h/52d Inode: 844424930132069  Links: 1
Access: (0000/l---------)  Uid: (    0/    root)   Gid: (    0/    root)
Access: 2020-10-21 17:46:51.839458900 -0500
Modify: 2020-10-21 17:46:51.839458900 -0500
Change: 2020-10-21 18:30:39.797358800 -0500
 Birth: -
  File: /mnt1/fifostat: cannot read symbolic link '/mnt1/fifo': Operation not supported

  Size: 0          Blocks: 0          IO Block: 16384  symbolic link
Device: 34h/52d Inode: 1125899906842722  Links: 1
Access: (0000/l---------)  Uid: (    0/    root)   Gid: (    0/    root)
Access: 2020-10-21 16:21:37.259249700 -0500
Modify: 2020-10-21 16:21:37.259249700 -0500
Change: 2020-10-21 18:30:39.797358800 -0500
 Birth: -
  File: /mnt1/blockstat: cannot read symbolic link '/mnt1/block': Operation not supported

  Size: 0          Blocks: 0          IO Block: 16384  symbolic link
Device: 34h/52d Inode: 844424930132068  Links: 1
Access: (0000/l---------)  Uid: (    0/    root)   Gid: (    0/    root)
Access: 2020-10-21 17:10:47.913103200 -0500
Modify: 2020-10-21 17:10:47.913103200 -0500
Change: 2020-10-21 18:30:39.796725500 -0500
Signed-off-by: default avatarSteve French <stfrench@microsoft.com>
Reviewed-by: default avatarRonnie Sahlberg <lsahlber@redhat.com>
parent 0613ed91
...@@ -298,6 +298,10 @@ struct smb_version_operations { ...@@ -298,6 +298,10 @@ struct smb_version_operations {
/* query file data from the server */ /* query file data from the server */
int (*query_file_info)(const unsigned int, struct cifs_tcon *, int (*query_file_info)(const unsigned int, struct cifs_tcon *,
struct cifs_fid *, FILE_ALL_INFO *); struct cifs_fid *, FILE_ALL_INFO *);
/* query reparse tag from srv to determine which type of special file */
int (*query_reparse_tag)(const unsigned int xid, struct cifs_tcon *tcon,
struct cifs_sb_info *cifs_sb, const char *path,
__u32 *reparse_tag);
/* get server index number */ /* get server index number */
int (*get_srv_inum)(const unsigned int, struct cifs_tcon *, int (*get_srv_inum)(const unsigned int, struct cifs_tcon *,
struct cifs_sb_info *, const char *, struct cifs_sb_info *, const char *,
......
...@@ -656,7 +656,7 @@ smb311_posix_info_to_fattr(struct cifs_fattr *fattr, struct smb311_posix_qinfo * ...@@ -656,7 +656,7 @@ smb311_posix_info_to_fattr(struct cifs_fattr *fattr, struct smb311_posix_qinfo *
static void static void
cifs_all_info_to_fattr(struct cifs_fattr *fattr, FILE_ALL_INFO *info, cifs_all_info_to_fattr(struct cifs_fattr *fattr, FILE_ALL_INFO *info,
struct super_block *sb, bool adjust_tz, struct super_block *sb, bool adjust_tz,
bool symlink) bool symlink, u32 reparse_tag)
{ {
struct cifs_sb_info *cifs_sb = CIFS_SB(sb); struct cifs_sb_info *cifs_sb = CIFS_SB(sb);
struct cifs_tcon *tcon = cifs_sb_master_tcon(cifs_sb); struct cifs_tcon *tcon = cifs_sb_master_tcon(cifs_sb);
...@@ -684,8 +684,22 @@ cifs_all_info_to_fattr(struct cifs_fattr *fattr, FILE_ALL_INFO *info, ...@@ -684,8 +684,22 @@ cifs_all_info_to_fattr(struct cifs_fattr *fattr, FILE_ALL_INFO *info,
fattr->cf_createtime = le64_to_cpu(info->CreationTime); fattr->cf_createtime = le64_to_cpu(info->CreationTime);
fattr->cf_nlink = le32_to_cpu(info->NumberOfLinks); fattr->cf_nlink = le32_to_cpu(info->NumberOfLinks);
if (reparse_tag == IO_REPARSE_TAG_LX_SYMLINK) {
if (symlink) { fattr->cf_mode |= S_IFLNK | cifs_sb->mnt_file_mode;
fattr->cf_dtype = DT_LNK;
} else if (reparse_tag == IO_REPARSE_TAG_LX_FIFO) {
fattr->cf_mode |= S_IFIFO | cifs_sb->mnt_file_mode;
fattr->cf_dtype = DT_FIFO;
} else if (reparse_tag == IO_REPARSE_TAG_AF_UNIX) {
fattr->cf_mode |= S_IFSOCK | cifs_sb->mnt_file_mode;
fattr->cf_dtype = DT_SOCK;
} else if (reparse_tag == IO_REPARSE_TAG_LX_CHR) {
fattr->cf_mode |= S_IFCHR | cifs_sb->mnt_file_mode;
fattr->cf_dtype = DT_CHR;
} else if (reparse_tag == IO_REPARSE_TAG_LX_BLK) {
fattr->cf_mode |= S_IFBLK | cifs_sb->mnt_file_mode;
fattr->cf_dtype = DT_BLK;
} else if (symlink) { /* TODO add more reparse tag checks */
fattr->cf_mode = S_IFLNK; fattr->cf_mode = S_IFLNK;
fattr->cf_dtype = DT_LNK; fattr->cf_dtype = DT_LNK;
} else if (fattr->cf_cifsattrs & ATTR_DIRECTORY) { } else if (fattr->cf_cifsattrs & ATTR_DIRECTORY) {
...@@ -740,8 +754,9 @@ cifs_get_file_info(struct file *filp) ...@@ -740,8 +754,9 @@ cifs_get_file_info(struct file *filp)
rc = server->ops->query_file_info(xid, tcon, &cfile->fid, &find_data); rc = server->ops->query_file_info(xid, tcon, &cfile->fid, &find_data);
switch (rc) { switch (rc) {
case 0: case 0:
/* TODO: add support to query reparse tag */
cifs_all_info_to_fattr(&fattr, &find_data, inode->i_sb, false, cifs_all_info_to_fattr(&fattr, &find_data, inode->i_sb, false,
false); false, 0 /* no reparse tag */);
break; break;
case -EREMOTE: case -EREMOTE:
cifs_create_dfs_fattr(&fattr, inode->i_sb); cifs_create_dfs_fattr(&fattr, inode->i_sb);
...@@ -910,12 +925,13 @@ cifs_get_inode_info(struct inode **inode, ...@@ -910,12 +925,13 @@ cifs_get_inode_info(struct inode **inode,
struct cifs_sb_info *cifs_sb = CIFS_SB(sb); struct cifs_sb_info *cifs_sb = CIFS_SB(sb);
bool adjust_tz = false; bool adjust_tz = false;
struct cifs_fattr fattr = {0}; struct cifs_fattr fattr = {0};
bool symlink = false; bool is_reparse_point = false;
FILE_ALL_INFO *data = in_data; FILE_ALL_INFO *data = in_data;
FILE_ALL_INFO *tmp_data = NULL; FILE_ALL_INFO *tmp_data = NULL;
void *smb1_backup_rsp_buf = NULL; void *smb1_backup_rsp_buf = NULL;
int rc = 0; int rc = 0;
int tmprc = 0; int tmprc = 0;
__u32 reparse_tag = 0;
tlink = cifs_sb_tlink(cifs_sb); tlink = cifs_sb_tlink(cifs_sb);
if (IS_ERR(tlink)) if (IS_ERR(tlink))
...@@ -938,8 +954,8 @@ cifs_get_inode_info(struct inode **inode, ...@@ -938,8 +954,8 @@ cifs_get_inode_info(struct inode **inode,
goto out; goto out;
} }
rc = server->ops->query_path_info(xid, tcon, cifs_sb, rc = server->ops->query_path_info(xid, tcon, cifs_sb,
full_path, tmp_data, full_path, tmp_data,
&adjust_tz, &symlink); &adjust_tz, &is_reparse_point);
data = tmp_data; data = tmp_data;
} }
...@@ -949,7 +965,19 @@ cifs_get_inode_info(struct inode **inode, ...@@ -949,7 +965,19 @@ cifs_get_inode_info(struct inode **inode,
switch (rc) { switch (rc) {
case 0: case 0:
cifs_all_info_to_fattr(&fattr, data, sb, adjust_tz, symlink); /*
* If the file is a reparse point, it is more complicated
* since we have to check if its reparse tag matches a known
* special file type e.g. symlink or fifo or char etc.
*/
if ((le32_to_cpu(data->Attributes) & ATTR_REPARSE) &&
server->ops->query_reparse_tag) {
rc = server->ops->query_reparse_tag(xid, tcon, cifs_sb,
full_path, &reparse_tag);
cifs_dbg(FYI, "reparse tag 0x%x\n", reparse_tag);
}
cifs_all_info_to_fattr(&fattr, data, sb, adjust_tz,
is_reparse_point, reparse_tag);
break; break;
case -EREMOTE: case -EREMOTE:
/* DFS link, no metadata available on this server */ /* DFS link, no metadata available on this server */
......
...@@ -506,7 +506,7 @@ move_smb2_info_to_cifs(FILE_ALL_INFO *dst, struct smb2_file_all_info *src) ...@@ -506,7 +506,7 @@ move_smb2_info_to_cifs(FILE_ALL_INFO *dst, struct smb2_file_all_info *src)
int int
smb2_query_path_info(const unsigned int xid, struct cifs_tcon *tcon, smb2_query_path_info(const unsigned int xid, struct cifs_tcon *tcon,
struct cifs_sb_info *cifs_sb, const char *full_path, struct cifs_sb_info *cifs_sb, const char *full_path,
FILE_ALL_INFO *data, bool *adjust_tz, bool *symlink) FILE_ALL_INFO *data, bool *adjust_tz, bool *reparse)
{ {
int rc; int rc;
struct smb2_file_all_info *smb2_data; struct smb2_file_all_info *smb2_data;
...@@ -516,7 +516,7 @@ smb2_query_path_info(const unsigned int xid, struct cifs_tcon *tcon, ...@@ -516,7 +516,7 @@ smb2_query_path_info(const unsigned int xid, struct cifs_tcon *tcon,
struct cached_fid *cfid = NULL; struct cached_fid *cfid = NULL;
*adjust_tz = false; *adjust_tz = false;
*symlink = false; *reparse = false;
smb2_data = kzalloc(sizeof(struct smb2_file_all_info) + PATH_MAX * 2, smb2_data = kzalloc(sizeof(struct smb2_file_all_info) + PATH_MAX * 2,
GFP_KERNEL); GFP_KERNEL);
...@@ -548,7 +548,7 @@ smb2_query_path_info(const unsigned int xid, struct cifs_tcon *tcon, ...@@ -548,7 +548,7 @@ smb2_query_path_info(const unsigned int xid, struct cifs_tcon *tcon,
FILE_READ_ATTRIBUTES, FILE_OPEN, create_options, FILE_READ_ATTRIBUTES, FILE_OPEN, create_options,
ACL_NO_MODE, smb2_data, SMB2_OP_QUERY_INFO, cfile); ACL_NO_MODE, smb2_data, SMB2_OP_QUERY_INFO, cfile);
if (rc == -EOPNOTSUPP) { if (rc == -EOPNOTSUPP) {
*symlink = true; *reparse = true;
create_options |= OPEN_REPARSE_POINT; create_options |= OPEN_REPARSE_POINT;
/* Failed on a symbolic link - query a reparse point info */ /* Failed on a symbolic link - query a reparse point info */
...@@ -570,7 +570,7 @@ smb2_query_path_info(const unsigned int xid, struct cifs_tcon *tcon, ...@@ -570,7 +570,7 @@ smb2_query_path_info(const unsigned int xid, struct cifs_tcon *tcon,
int int
smb311_posix_query_path_info(const unsigned int xid, struct cifs_tcon *tcon, smb311_posix_query_path_info(const unsigned int xid, struct cifs_tcon *tcon,
struct cifs_sb_info *cifs_sb, const char *full_path, struct cifs_sb_info *cifs_sb, const char *full_path,
struct smb311_posix_qinfo *data, bool *adjust_tz, bool *symlink) struct smb311_posix_qinfo *data, bool *adjust_tz, bool *reparse)
{ {
int rc; int rc;
__u32 create_options = 0; __u32 create_options = 0;
...@@ -578,7 +578,7 @@ smb311_posix_query_path_info(const unsigned int xid, struct cifs_tcon *tcon, ...@@ -578,7 +578,7 @@ smb311_posix_query_path_info(const unsigned int xid, struct cifs_tcon *tcon,
struct smb311_posix_qinfo *smb2_data; struct smb311_posix_qinfo *smb2_data;
*adjust_tz = false; *adjust_tz = false;
*symlink = false; *reparse = false;
/* BB TODO: Make struct larger when add support for parsing owner SIDs */ /* BB TODO: Make struct larger when add support for parsing owner SIDs */
smb2_data = kzalloc(sizeof(struct smb311_posix_qinfo), smb2_data = kzalloc(sizeof(struct smb311_posix_qinfo),
...@@ -599,7 +599,7 @@ smb311_posix_query_path_info(const unsigned int xid, struct cifs_tcon *tcon, ...@@ -599,7 +599,7 @@ smb311_posix_query_path_info(const unsigned int xid, struct cifs_tcon *tcon,
ACL_NO_MODE, smb2_data, SMB2_OP_POSIX_QUERY_INFO, cfile); ACL_NO_MODE, smb2_data, SMB2_OP_POSIX_QUERY_INFO, cfile);
if (rc == -EOPNOTSUPP) { if (rc == -EOPNOTSUPP) {
/* BB TODO: When support for special files added to Samba re-verify this path */ /* BB TODO: When support for special files added to Samba re-verify this path */
*symlink = true; *reparse = true;
create_options |= OPEN_REPARSE_POINT; create_options |= OPEN_REPARSE_POINT;
/* Failed on a symbolic link - query a reparse point info */ /* Failed on a symbolic link - query a reparse point info */
......
...@@ -3034,6 +3034,138 @@ smb2_query_symlink(const unsigned int xid, struct cifs_tcon *tcon, ...@@ -3034,6 +3034,138 @@ smb2_query_symlink(const unsigned int xid, struct cifs_tcon *tcon,
return rc; return rc;
} }
int
smb2_query_reparse_tag(const unsigned int xid, struct cifs_tcon *tcon,
struct cifs_sb_info *cifs_sb, const char *full_path,
__u32 *tag)
{
int rc;
__le16 *utf16_path = NULL;
__u8 oplock = SMB2_OPLOCK_LEVEL_NONE;
struct cifs_open_parms oparms;
struct cifs_fid fid;
struct kvec err_iov = {NULL, 0};
struct TCP_Server_Info *server = cifs_pick_channel(tcon->ses);
int flags = 0;
struct smb_rqst rqst[3];
int resp_buftype[3];
struct kvec rsp_iov[3];
struct kvec open_iov[SMB2_CREATE_IOV_SIZE];
struct kvec io_iov[SMB2_IOCTL_IOV_SIZE];
struct kvec close_iov[1];
struct smb2_create_rsp *create_rsp;
struct smb2_ioctl_rsp *ioctl_rsp;
struct reparse_data_buffer *reparse_buf;
u32 plen;
cifs_dbg(FYI, "%s: path: %s\n", __func__, full_path);
if (smb3_encryption_required(tcon))
flags |= CIFS_TRANSFORM_REQ;
memset(rqst, 0, sizeof(rqst));
resp_buftype[0] = resp_buftype[1] = resp_buftype[2] = CIFS_NO_BUFFER;
memset(rsp_iov, 0, sizeof(rsp_iov));
utf16_path = cifs_convert_path_to_utf16(full_path, cifs_sb);
if (!utf16_path)
return -ENOMEM;
/*
* setup smb2open - TODO add optimization to call cifs_get_readable_path
* to see if there is a handle already open that we can use
*/
memset(&open_iov, 0, sizeof(open_iov));
rqst[0].rq_iov = open_iov;
rqst[0].rq_nvec = SMB2_CREATE_IOV_SIZE;
memset(&oparms, 0, sizeof(oparms));
oparms.tcon = tcon;
oparms.desired_access = FILE_READ_ATTRIBUTES;
oparms.disposition = FILE_OPEN;
oparms.create_options = cifs_create_options(cifs_sb, OPEN_REPARSE_POINT);
oparms.fid = &fid;
oparms.reconnect = false;
rc = SMB2_open_init(tcon, server,
&rqst[0], &oplock, &oparms, utf16_path);
if (rc)
goto query_rp_exit;
smb2_set_next_command(tcon, &rqst[0]);
/* IOCTL */
memset(&io_iov, 0, sizeof(io_iov));
rqst[1].rq_iov = io_iov;
rqst[1].rq_nvec = SMB2_IOCTL_IOV_SIZE;
rc = SMB2_ioctl_init(tcon, server,
&rqst[1], fid.persistent_fid,
fid.volatile_fid, FSCTL_GET_REPARSE_POINT,
true /* is_fctl */, NULL, 0,
CIFSMaxBufSize -
MAX_SMB2_CREATE_RESPONSE_SIZE -
MAX_SMB2_CLOSE_RESPONSE_SIZE);
if (rc)
goto query_rp_exit;
smb2_set_next_command(tcon, &rqst[1]);
smb2_set_related(&rqst[1]);
/* Close */
memset(&close_iov, 0, sizeof(close_iov));
rqst[2].rq_iov = close_iov;
rqst[2].rq_nvec = 1;
rc = SMB2_close_init(tcon, server,
&rqst[2], COMPOUND_FID, COMPOUND_FID, false);
if (rc)
goto query_rp_exit;
smb2_set_related(&rqst[2]);
rc = compound_send_recv(xid, tcon->ses, server,
flags, 3, rqst,
resp_buftype, rsp_iov);
create_rsp = rsp_iov[0].iov_base;
if (create_rsp && create_rsp->sync_hdr.Status)
err_iov = rsp_iov[0];
ioctl_rsp = rsp_iov[1].iov_base;
/*
* Open was successful and we got an ioctl response.
*/
if (rc == 0) {
/* See MS-FSCC 2.3.23 */
reparse_buf = (struct reparse_data_buffer *)
((char *)ioctl_rsp +
le32_to_cpu(ioctl_rsp->OutputOffset));
plen = le32_to_cpu(ioctl_rsp->OutputCount);
if (plen + le32_to_cpu(ioctl_rsp->OutputOffset) >
rsp_iov[1].iov_len) {
cifs_tcon_dbg(FYI, "srv returned invalid ioctl len: %d\n",
plen);
rc = -EIO;
goto query_rp_exit;
}
*tag = le32_to_cpu(reparse_buf->ReparseTag);
}
query_rp_exit:
kfree(utf16_path);
SMB2_open_free(&rqst[0]);
SMB2_ioctl_free(&rqst[1]);
SMB2_close_free(&rqst[2]);
free_rsp_buf(resp_buftype[0], rsp_iov[0].iov_base);
free_rsp_buf(resp_buftype[1], rsp_iov[1].iov_base);
free_rsp_buf(resp_buftype[2], rsp_iov[2].iov_base);
return rc;
}
static struct cifs_ntsd * static struct cifs_ntsd *
get_smb2_acl_by_fid(struct cifs_sb_info *cifs_sb, get_smb2_acl_by_fid(struct cifs_sb_info *cifs_sb,
const struct cifs_fid *cifsfid, u32 *pacllen) const struct cifs_fid *cifsfid, u32 *pacllen)
...@@ -4986,6 +5118,8 @@ struct smb_version_operations smb30_operations = { ...@@ -4986,6 +5118,8 @@ struct smb_version_operations smb30_operations = {
.can_echo = smb2_can_echo, .can_echo = smb2_can_echo,
.echo = SMB2_echo, .echo = SMB2_echo,
.query_path_info = smb2_query_path_info, .query_path_info = smb2_query_path_info,
/* WSL tags introduced long after smb2.1, enable for SMB3, 3.11 only */
.query_reparse_tag = smb2_query_reparse_tag,
.get_srv_inum = smb2_get_srv_inum, .get_srv_inum = smb2_get_srv_inum,
.query_file_info = smb2_query_file_info, .query_file_info = smb2_query_file_info,
.set_path_size = smb2_set_path_size, .set_path_size = smb2_set_path_size,
...@@ -5097,6 +5231,7 @@ struct smb_version_operations smb311_operations = { ...@@ -5097,6 +5231,7 @@ struct smb_version_operations smb311_operations = {
.can_echo = smb2_can_echo, .can_echo = smb2_can_echo,
.echo = SMB2_echo, .echo = SMB2_echo,
.query_path_info = smb2_query_path_info, .query_path_info = smb2_query_path_info,
.query_reparse_tag = smb2_query_reparse_tag,
.get_srv_inum = smb2_get_srv_inum, .get_srv_inum = smb2_get_srv_inum,
.query_file_info = smb2_query_file_info, .query_file_info = smb2_query_file_info,
.set_path_size = smb2_set_path_size, .set_path_size = smb2_set_path_size,
......
...@@ -1691,6 +1691,11 @@ struct smb2_file_eof_info { /* encoding of request for level 10 */ ...@@ -1691,6 +1691,11 @@ struct smb2_file_eof_info { /* encoding of request for level 10 */
__le64 EndOfFile; /* new end of file value */ __le64 EndOfFile; /* new end of file value */
} __packed; /* level 20 Set */ } __packed; /* level 20 Set */
struct smb2_file_reparse_point_info {
__le64 IndexNumber;
__le32 Tag;
} __packed;
struct smb2_file_network_open_info { struct smb2_file_network_open_info {
__le64 CreationTime; __le64 CreationTime;
__le64 LastAccessTime; __le64 LastAccessTime;
......
...@@ -77,6 +77,9 @@ extern void close_shroot_lease(struct cached_fid *cfid); ...@@ -77,6 +77,9 @@ extern void close_shroot_lease(struct cached_fid *cfid);
extern void close_shroot_lease_locked(struct cached_fid *cfid); extern void close_shroot_lease_locked(struct cached_fid *cfid);
extern void move_smb2_info_to_cifs(FILE_ALL_INFO *dst, extern void move_smb2_info_to_cifs(FILE_ALL_INFO *dst,
struct smb2_file_all_info *src); struct smb2_file_all_info *src);
extern int smb2_query_reparse_tag(const unsigned int xid, struct cifs_tcon *tcon,
struct cifs_sb_info *cifs_sb, const char *path,
__u32 *reparse_tag);
extern int smb2_query_path_info(const unsigned int xid, struct cifs_tcon *tcon, extern int smb2_query_path_info(const unsigned int xid, struct cifs_tcon *tcon,
struct cifs_sb_info *cifs_sb, struct cifs_sb_info *cifs_sb,
const char *full_path, FILE_ALL_INFO *data, const char *full_path, FILE_ALL_INFO *data,
......
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