1210 lines
30 KiB
C
1210 lines
30 KiB
C
/*
|
|
* UltraDefrag - powerful defragmentation tool for Windows NT.
|
|
* Copyright (c) 2011 by Jean-Pierre Andre for the Linux version
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; either version 2 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program; if not, write to the Free Software
|
|
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
|
|
*/
|
|
|
|
/*
|
|
* Interface with libntfs-3g
|
|
*
|
|
* To avoid symbol conflicts, every interface must go through
|
|
* this module, and must only rely on types defined in compiler.h
|
|
* (universal C types tolerated)
|
|
*
|
|
* Only trace level 3 should be used
|
|
*/
|
|
|
|
#include "compiler.h"
|
|
#if LXSC | L8SC | STSC | SPGC | PPGC | ARGC
|
|
#include "config.h"
|
|
#endif
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <stdlib.h>
|
|
#include <stdarg.h>
|
|
#include <sys/stat.h>
|
|
#include <errno.h>
|
|
|
|
#include "layout.h"
|
|
#include "attrib.h"
|
|
#include "volume.h"
|
|
#include "mft.h"
|
|
#include "dir.h"
|
|
#include "inode.h"
|
|
#include "bitmap.h"
|
|
#include "lcnalloc.h"
|
|
#include "ntfstime.h"
|
|
#include "ntfs-3g.h"
|
|
|
|
#if (LXSC | L8SC | STSC | SPGC | ARGC) & !NOMEMCHK
|
|
|
|
void *ntfs_mymalloc(size_t, const char*, int); /* extra relay desirable */
|
|
void *ntfs_mycalloc(size_t,size_t, const char*, int); /* extra relay desirable */
|
|
void *ntfs_myrealloc(void*,size_t, const char*, int); /* extra relay desirable */
|
|
void ntfs_myfree(void*, const char*, int); /* extra relay desirable */
|
|
char *ntfs_mystrdup(const char*, const char*, int);
|
|
#define malloc(sz) ntfs_mymalloc(sz,__FILE__,__LINE__)
|
|
#define calloc(sz,cnt) ntfs_mycalloc(sz,cnt,__FILE__,__LINE__)
|
|
#define realloc(old,sz) ntfs_myrealloc(old,sz,__FILE__,__LINE__)
|
|
#define free(old) ntfs_myfree(old,__FILE__,__LINE__)
|
|
#define ntfs_malloc(sz) ntfs_mymalloc(sz,__FILE__,__LINE__)
|
|
#define ntfs_calloc(sz,cnt) ntfs_mymalloc(sz,cnt,__FILE__,__LINE__)
|
|
#define ntfs_realloc(old,sz) ntfs_mymalloc(old,sz,__FILE__,__LINE__)
|
|
#define ntfs_free(old) ntfs_myfree(old,__FILE__,__LINE__)
|
|
#undef strdup
|
|
#define strdup(p) ntfs_mystrdup(p,__FILE__,__LINE__)
|
|
|
|
#else /* LXSC | L8SC | STSC | SPGC | ARGC */
|
|
|
|
void *ntfs_malloc(size_t);
|
|
void *ntfs_calloc(size_t);
|
|
void *ntfs_realloc(size_t,void*);
|
|
#define malloc(sz) ntfs_malloc(sz)
|
|
#define calloc(sz,cnt) ntfs_calloc(sz*cnt)
|
|
#define realloc(sz,old) ntfs_realloc(sz,old)
|
|
|
|
#endif /* LXSC | L8SC | STSC | SPGC | ARGC */
|
|
|
|
/* temporary : this module should never get out, just return conditions */
|
|
void stop_there(int, const char*, int);
|
|
#define get_out(n) stop_there(n,__FILE__,__LINE__)
|
|
|
|
|
|
struct TYPES_TABLE {
|
|
le32 type;
|
|
char *name;
|
|
} ;
|
|
|
|
/*
|
|
* What type of attribute should be searched for a given stream
|
|
* Only attribute which may be non-resident should be present here
|
|
*/
|
|
struct TYPES_TABLE types_table[] = {
|
|
{ AT_BITMAP, "$BITMAP" },
|
|
{ AT_ATTRIBUTE_LIST, "$ATTRIBUTE_LIST" },
|
|
{ AT_SECURITY_DESCRIPTOR, "$SECURITY_DESCRIPTOR" },
|
|
{ AT_REPARSE_POINT, "$REPARSE_POINT" },
|
|
{ AT_EA, "$EA" },
|
|
{ AT_EA_INFORMATION, "$EA_INFORMATION" },
|
|
{ AT_LOGGED_UTILITY_STREAM, "$LOGGED_UTILITY_STREAM" },
|
|
{ AT_INDEX_ALLOCATION, "$INDEX_ALLOCATION" },
|
|
{ AT_BITMAP, "$BITMAP" },
|
|
{ const_cpu_to_le32(0), (char*)NULL } /* end marker */
|
|
} ;
|
|
|
|
static ntfschar I30[] = { const_cpu_to_le16('$'), const_cpu_to_le16('I'),
|
|
const_cpu_to_le16('3'), const_cpu_to_le16('0') } ;
|
|
|
|
extern int trace;
|
|
extern int do_nothing;
|
|
|
|
extern int ntfs_toutf16(utf16_t*, const char*, int);
|
|
extern int ntfs_toutf8(char*, const utf_t*, int);
|
|
|
|
#if PPGC
|
|
|
|
#undef bswap_64
|
|
|
|
unsigned long long bswap_64(unsigned long long x)
|
|
{
|
|
unsigned long long y;
|
|
int i;
|
|
|
|
y = 0;
|
|
for (i=0; i<=56; i+=8)
|
|
y = (y << 8) + ((x >> i) & 255);
|
|
return (y);
|
|
}
|
|
|
|
#endif /* PPGC */
|
|
|
|
int ntfs_mounted_device(const char *path)
|
|
{
|
|
unsigned long existing_mount;
|
|
|
|
return (!ntfs_check_if_mounted(path, &existing_mount)
|
|
&& (existing_mount & NTFS_MF_MOUNTED));
|
|
}
|
|
|
|
HANDLE ntfs_open(const utf_t *device)
|
|
{
|
|
ntfs_volume *vol;
|
|
|
|
if (do_nothing)
|
|
vol = ntfs_mount(device,NTFS_MNT_RDONLY);
|
|
else
|
|
vol = ntfs_mount(device,0);
|
|
#ifdef NVolSetNoFixupWarn
|
|
if (vol)
|
|
NVolSetNoFixupWarn(vol);
|
|
#endif
|
|
if (trace > 2)
|
|
safe_fprintf(stderr,"device %s opened handle 0x%lx\n",device,(long)vol);
|
|
return ((HANDLE)vol);
|
|
}
|
|
|
|
int ntfs_close(HANDLE h)
|
|
{
|
|
ntfs_volume *vol;
|
|
|
|
/*
|
|
* Having relocated parts of the MFT, we have updated its
|
|
* mapping pairs, so the MFT is marked dirty and we have
|
|
* to sync it, which implies modifications to the parent
|
|
* directory, which in turn implies a modification to
|
|
* the MFT, and so on.
|
|
* So we first try a sync, then clear the dirty flags
|
|
* caused by the updating of parent directory, so that
|
|
* the volume can be closed with no error.
|
|
*/
|
|
vol = (ntfs_volume*)h;
|
|
ntfs_inode_sync(vol->mft_ni);
|
|
NInoFileNameClearDirty(vol->mft_ni);
|
|
NInoClearDirty(vol->mft_ni);
|
|
return (ntfs_umount(vol,FALSE));
|
|
}
|
|
|
|
int ntfs_sync(HANDLE h)
|
|
{
|
|
ntfs_volume *vol;
|
|
|
|
vol = (ntfs_volume*)h;
|
|
return (ntfs_device_sync(vol->dev));
|
|
}
|
|
|
|
/*
|
|
* Read from the device
|
|
*/
|
|
|
|
int ntfs_read(HANDLE h, char *buf, ULONG size, ULONGLONG pos)
|
|
{
|
|
ntfs_volume *vol;
|
|
int r;
|
|
|
|
r = -1;
|
|
if (h) {
|
|
vol = (ntfs_volume*)h;
|
|
if (ntfs_pread(vol->dev, pos, size, buf)
|
|
== (SLONGLONG)size)
|
|
r = 0;
|
|
}
|
|
if (r && (trace > 2))
|
|
safe_fprintf(stderr,"** Failed to read %lu bytes from device at 0x%llx\n",
|
|
(unsigned long)size,(long long)pos);
|
|
return (r);
|
|
}
|
|
|
|
/*
|
|
* Write to the device
|
|
*/
|
|
|
|
int ntfs_write(HANDLE h, const char *buf, ULONG size, ULONGLONG pos)
|
|
{
|
|
ntfs_volume *vol;
|
|
int r;
|
|
|
|
r = -1;
|
|
if (h) {
|
|
vol = (ntfs_volume*)h;
|
|
if (ntfs_pwrite(vol->dev, pos, size, buf)
|
|
== (SLONGLONG)size)
|
|
r = 0;
|
|
}
|
|
if (r && (trace > 2))
|
|
safe_fprintf(stderr,"** Failed to write %lu bytes to device at 0x%llx\n",
|
|
(unsigned long)size,(long long)pos);
|
|
return (r);
|
|
}
|
|
|
|
/*
|
|
* Create an inner file or attribute
|
|
*/
|
|
|
|
HANDLE ntfs_create_file(HANDLE h, const char *path)
|
|
{
|
|
char *name;
|
|
ntfschar *uname = NULL;
|
|
ntfs_volume *vol;
|
|
ntfs_inode *dir_ni = NULL, *ni;
|
|
ntfs_attr *na;
|
|
char *dir_path;
|
|
le32 securid;
|
|
int res = 0, uname_len;
|
|
|
|
if (trace > 2)
|
|
safe_fprintf(stderr,"ntfs_create_file %s\n",path);
|
|
na = (ntfs_attr*)NULL;
|
|
ni = (ntfs_inode*)NULL;
|
|
if (!h) {
|
|
res = -EINVAL;
|
|
goto reject;
|
|
}
|
|
vol = (HANDLE)h;
|
|
dir_path = strdup(path);
|
|
if (!dir_path) {
|
|
res = -errno;
|
|
goto reject;
|
|
}
|
|
/* Generate unicode filename. */
|
|
name = strrchr(dir_path, '/');
|
|
name++;
|
|
uname_len = ntfs_mbstoucs(name, &uname);
|
|
if (uname_len < 0) {
|
|
res = -errno;
|
|
goto exit;
|
|
}
|
|
/* Open parent directory. */
|
|
*--name = 0;
|
|
dir_ni = ntfs_pathname_to_inode(vol, NULL, dir_path);
|
|
if (!dir_ni) {
|
|
res = -errno;
|
|
goto exit;
|
|
}
|
|
securid = 0;
|
|
if (trace > 2)
|
|
safe_fprintf(stderr,"parent directory %s %ld\n",dir_path,(long)dir_ni->mft_no);
|
|
ni = ntfs_create(dir_ni, securid, uname, uname_len, S_IFREG);
|
|
if (ni) {
|
|
na = ntfs_attr_open(ni, AT_DATA, NULL, 0);
|
|
if (!na && !res)
|
|
res = errno;
|
|
NInoSetDirty(ni);
|
|
ntfs_inode_update_times(dir_ni, NTFS_UPDATE_MCTIME);
|
|
if (trace > 2)
|
|
safe_fprintf(stderr,"file %s created ni 0x%lx %ld na 0x%lx\n",path,
|
|
(long)ni,(long)ni->mft_no,(long)na);
|
|
} else
|
|
res = -errno;
|
|
exit:
|
|
free(uname);
|
|
if (ntfs_inode_close(dir_ni) && !res)
|
|
res = -errno;
|
|
if (!na && ntfs_inode_close(ni) && !res)
|
|
res = -errno;
|
|
free(dir_path);
|
|
reject :
|
|
return (res ? (HANDLE)NULL : (HANDLE)na);
|
|
}
|
|
|
|
static ntfs_attr *open_stream(ntfs_volume *vol, const char *name)
|
|
{
|
|
const char *suff;
|
|
ntfs_inode *ni;
|
|
ntfs_attr *na;
|
|
le32 type;
|
|
char *base_name;
|
|
char *post_name;
|
|
char *type_name;
|
|
BOOL dyn_name;
|
|
ntfschar *stream_name;
|
|
struct TYPES_TABLE *t;
|
|
int name_lth;
|
|
|
|
suff = strstr(name,"//");
|
|
type = const_cpu_to_le32(0);
|
|
ni = (ntfs_inode*)NULL;
|
|
dyn_name = FALSE;
|
|
if (suff && suff[2]) {
|
|
base_name = strdup(name);
|
|
stream_name = (ntfschar*)NULL;
|
|
type_name = (char*)NULL;
|
|
name_lth = 0;
|
|
if (base_name) {
|
|
post_name = strstr(base_name,"//");
|
|
if (post_name) {
|
|
*post_name++ = 0;
|
|
post_name++;
|
|
ni = ntfs_pathname_to_inode(vol,NULL,base_name);
|
|
if (trace > 2)
|
|
safe_fprintf(stderr,"base_name %s ni 0x%lx %lld stream %s\n",
|
|
base_name,(long)ni,
|
|
(long long)ni->mft_no,post_name);
|
|
type_name = strchr(post_name,'/');
|
|
if (type_name)
|
|
*type_name++ = 0;
|
|
}
|
|
}
|
|
if (ni) {
|
|
t = types_table;
|
|
if (type_name)
|
|
while (t->type && strcmp(t->name,type_name))
|
|
t++;
|
|
if (type_name
|
|
&& t->type
|
|
&& (t->type != AT_INDEX_ALLOCATION)) {
|
|
type = t->type;
|
|
} else {
|
|
/* named data or directory stream */
|
|
stream_name = (ntfschar*)ntfs_malloc(2*strlen(post_name));
|
|
if (stream_name) {
|
|
name_lth = ntfs_toutf16(stream_name,post_name,strlen(post_name));
|
|
dyn_name = TRUE;
|
|
}
|
|
if (t->type == AT_INDEX_ALLOCATION)
|
|
type = AT_INDEX_ALLOCATION;
|
|
else
|
|
type = AT_DATA;
|
|
}
|
|
if (!type) {
|
|
ntfs_inode_close(ni);
|
|
ni = (ntfs_inode*)NULL;
|
|
}
|
|
}
|
|
if (base_name)
|
|
free(base_name);
|
|
} else {
|
|
ni = ntfs_pathname_to_inode(vol,NULL,name);
|
|
if (ni && (ni->mrec->flags & MFT_RECORD_IS_DIRECTORY)) {
|
|
type = AT_INDEX_ALLOCATION;
|
|
stream_name = I30;
|
|
name_lth = 4;
|
|
} else {
|
|
type = AT_DATA;
|
|
stream_name = (ntfschar*)NULL;
|
|
name_lth = 0;
|
|
}
|
|
}
|
|
if (type && ni) {
|
|
na = ntfs_attr_open(ni, type, stream_name, name_lth);
|
|
if (!na) {
|
|
ntfs_inode_close(ni);
|
|
if (trace > 2)
|
|
safe_fprintf(stderr,"** Could not open %s\n",name);
|
|
}
|
|
if (dyn_name)
|
|
free(stream_name);
|
|
} else {
|
|
if (trace > 2)
|
|
safe_fprintf(stderr,"** Unsupported stream opening %s\n",name);
|
|
na = (ntfs_attr*)NULL;
|
|
}
|
|
return (na);
|
|
}
|
|
|
|
/*
|
|
* Open an inner file (its data or an attribute)
|
|
*/
|
|
|
|
HANDLE ntfs_open_file(HANDLE h, const char *name)
|
|
{
|
|
ntfs_volume *vol;
|
|
ntfs_attr *na;
|
|
|
|
na = (ntfs_attr*)NULL;
|
|
if (h) {
|
|
if (trace > 2)
|
|
safe_fprintf(stderr,"ntfs_open_file %s\n",name);
|
|
vol = (ntfs_volume*)h;
|
|
/*
|
|
* A few system files are kept open, do not
|
|
* reopen them.
|
|
* Moreover $DATA from $MFTMirr and $LogFile have
|
|
* runlists to be replicated into $MFTMirr, do not
|
|
* process them until replication is implemented
|
|
*/
|
|
if (!strcmp(name,"/$MFT")
|
|
|| !strncmp(name,"/$MFT//",7)
|
|
|| !strcmp(name,"/$MFTMirr")
|
|
|| !strcmp(name,"/$LogFile")
|
|
|| !strcmp(name,"/$Bitmap")) {
|
|
if (!strcmp(name,"/$MFT"))
|
|
na = vol->mft_na;
|
|
else
|
|
if (!strcmp(name,"/$MFT///$BITMAP"))
|
|
na = vol->mftbmp_na;
|
|
else
|
|
if (!strcmp(name,"/$Bitmap"))
|
|
na = vol->lcnbmp_na;
|
|
/* else return NULL to mean locked */
|
|
if (na && (trace > 2))
|
|
safe_fprintf(stderr,"Special file %s, na 0x%lx ni 0x%lx inode %lld\n",
|
|
name,(long)na,(long)na->ni,
|
|
(long long)na->ni->mft_no);
|
|
} else
|
|
na = open_stream(vol,name);
|
|
}
|
|
return ((HANDLE)na);
|
|
}
|
|
|
|
int ntfs_close_file(HANDLE h)
|
|
{
|
|
ntfs_volume *vol;
|
|
ntfs_inode *ni;
|
|
ntfs_attr *na;
|
|
void *buf;
|
|
int r;
|
|
|
|
r = -EINVAL;
|
|
na = (ntfs_attr*)h;
|
|
if (na && na->ni && na->ni->vol) {
|
|
ni = na->ni;
|
|
/*
|
|
* Do not close system streams which must be kept open
|
|
*/
|
|
switch (na->ni->mft_no) {
|
|
case FILE_MFT :
|
|
vol = ni->vol;
|
|
ntfs_inode_sync(ni);
|
|
buf = (void*)ntfs_malloc(vol->mft_record_size);
|
|
if (trace > 2)
|
|
safe_fprintf(stderr,"Copying MFT record to MFTMirr\n");
|
|
if (buf
|
|
&& (ntfs_attr_pread(vol->mft_na, 0,
|
|
vol->mft_record_size, buf)
|
|
== vol->mft_record_size)
|
|
&& (ntfs_attr_pwrite(vol->mftmirr_na, 0,
|
|
vol->mft_record_size, buf)
|
|
== vol->mft_record_size))
|
|
r = 0;
|
|
free(buf);
|
|
if (trace > 2)
|
|
safe_fprintf(stderr,"Avoiding closing ni 0x%lx inode %lld na 0x%lx\n",(long)ni,(long long)ni->mft_no,(long)na);
|
|
break;
|
|
case FILE_Bitmap :
|
|
if (trace > 2)
|
|
safe_fprintf(stderr,"Avoiding closing ni 0x%lx inode %lld na 0x%lx\n",(long)ni,(long long)ni->mft_no,(long)na);
|
|
break;
|
|
default :
|
|
ntfs_attr_close(na);
|
|
if (trace > 2)
|
|
safe_fprintf(stderr,"Closing ni 0x%lx inode %lld na 0x%lx\n",(long)ni,(long long)ni->mft_no,(long)na);
|
|
r = ntfs_inode_close(ni);
|
|
break;
|
|
}
|
|
}
|
|
if (trace > 2)
|
|
safe_fprintf(stderr,"inode closed, r %d\n",r);
|
|
return (r);
|
|
}
|
|
|
|
/*
|
|
* Write to an inner file
|
|
*/
|
|
|
|
int ntfs_write_file(HANDLE h, const char *buf, ULONG size, ULONGLONG offset)
|
|
{
|
|
ntfs_inode *ni;
|
|
ntfs_attr *na;
|
|
int res;
|
|
ULONGLONG total;
|
|
|
|
res = 0;
|
|
total = 0;
|
|
if (h) {
|
|
na = (ntfs_attr*)h;
|
|
ni = na->ni;
|
|
if (trace > 2)
|
|
safe_fprintf(stderr,"writing %ld bytes into ni 0x%lx %ld\n",(long)size,(long)ni,(long)ni->mft_no);
|
|
while (size) {
|
|
s64 ret = ntfs_attr_pwrite(na, offset, size, buf + total);
|
|
if (ret <= 0) {
|
|
res = -errno;
|
|
goto exit;
|
|
}
|
|
size -= ret;
|
|
offset += ret;
|
|
total += ret;
|
|
}
|
|
ntfs_inode_update_times(ni, NTFS_UPDATE_MCTIME);
|
|
exit:
|
|
if (trace > 2)
|
|
safe_fprintf(stderr,"written %ld bytes res %d\n",(long)total,(int)res);
|
|
}
|
|
return (res);
|
|
}
|
|
|
|
/*
|
|
* Unlink an inner file
|
|
*/
|
|
|
|
int ntfs_unlink(HANDLE h, const char *org_path)
|
|
{
|
|
char *name;
|
|
ntfs_volume *vol;
|
|
ntfschar *uname = NULL;
|
|
ntfs_inode *dir_ni = NULL, *ni;
|
|
char *path;
|
|
int res = 0, uname_len;
|
|
|
|
if (trace > 2)
|
|
safe_fprintf(stderr,"unlinking %s\n",org_path);
|
|
vol = (ntfs_volume*)h;
|
|
path = strdup(org_path);
|
|
if (!path)
|
|
return -errno;
|
|
/* Open object for delete. */
|
|
ni = ntfs_pathname_to_inode(vol, NULL, path);
|
|
if (!ni) {
|
|
res = -errno;
|
|
goto exit;
|
|
}
|
|
if (trace > 2)
|
|
safe_fprintf(stderr,"ni 0x%lx inode %ld\n",(long)ni,(long)ni->mft_no);
|
|
/* deny unlinking metadata files */
|
|
if (ni->mft_no < FILE_first_user) {
|
|
errno = EPERM;
|
|
res = -errno;
|
|
goto exit;
|
|
}
|
|
|
|
/* Generate unicode filename. */
|
|
name = strrchr(path, '/');
|
|
name++;
|
|
uname_len = ntfs_mbstoucs(name, &uname);
|
|
if (uname_len < 0) {
|
|
res = -errno;
|
|
goto exit;
|
|
}
|
|
/* Open parent directory. */
|
|
*--name = 0;
|
|
dir_ni = ntfs_pathname_to_inode(vol, NULL, path);
|
|
if (!dir_ni) {
|
|
res = -errno;
|
|
goto exit;
|
|
}
|
|
if (trace > 2)
|
|
safe_fprintf(stderr,"dir_ni 0x%lx inode %ld\n",(long)dir_ni,(long)dir_ni->mft_no);
|
|
if (ntfs_delete(vol, org_path, ni, dir_ni,
|
|
uname, uname_len))
|
|
res = -errno;
|
|
/* ntfs_delete() always closes ni and dir_ni */
|
|
ni = dir_ni = NULL;
|
|
exit:
|
|
if (ntfs_inode_close(dir_ni) && !res)
|
|
res = -errno;
|
|
if (ntfs_inode_close(ni) && !res)
|
|
res = -errno;
|
|
free(uname);
|
|
free(path);
|
|
if (trace > 2)
|
|
safe_fprintf(stderr,"delete res %d\n",res);
|
|
return res;
|
|
}
|
|
|
|
/*
|
|
* get the file system sizes.
|
|
* Returns zero if successful
|
|
*/
|
|
|
|
int ntfs_fill_fs_sizes(HANDLE h, LARGE_INTEGER *ptotal, LARGE_INTEGER *pavail,
|
|
ULONG *psectperclust, ULONG *pbytespersect)
|
|
{
|
|
ntfs_volume *vol;
|
|
int r;
|
|
|
|
if (h) {
|
|
vol = (ntfs_volume*)h;
|
|
if (trace > 2)
|
|
safe_fprintf(stderr,"vol 0x%lx na 0x%lx clusters %lld\n",(long)vol,(long)vol->lcnbmp_na,vol->nr_clusters);
|
|
vol->free_clusters = ntfs_attr_get_free_bits(vol->lcnbmp_na);
|
|
if (trace > 2)
|
|
safe_fprintf(stderr,"free_clusters %lld\n",vol->free_clusters);
|
|
ptotal->QuadPart = vol->nr_clusters;
|
|
pavail->QuadPart = vol->free_clusters;
|
|
*psectperclust = vol->cluster_size >> vol->sector_size_bits;
|
|
*pbytespersect = vol->sector_size;
|
|
if (trace > 2)
|
|
safe_fprintf(stderr,"total %lld avail %lld spc %ld bps %ld\n",
|
|
(long long)ptotal->QuadPart,(long long)pavail->QuadPart,
|
|
(long)*psectperclust,(long)*pbytespersect);
|
|
r = 0;
|
|
} else {
|
|
r = -1;
|
|
errno = EINVAL;
|
|
}
|
|
return (r);
|
|
}
|
|
|
|
int ntfs_fill_vol_info(HANDLE h, LARGE_INTEGER *pcrtime, ULONG *pserial,
|
|
ULONG *plablth, UCHAR *psupobj, WCHAR *plabel)
|
|
{
|
|
NTFS_BOOT_SECTOR *bs;
|
|
ULONGLONG serial;
|
|
ntfs_volume *vol;
|
|
ntfs_inode *ni;
|
|
int r;
|
|
|
|
if (h) {
|
|
vol = (ntfs_volume*)h;
|
|
ni = vol->vol_ni;
|
|
serial = 0;
|
|
bs = (NTFS_BOOT_SECTOR*)ntfs_malloc(sizeof(NTFS_BOOT_SECTOR));
|
|
if (bs) {
|
|
if (ntfs_pread(vol->dev, 0, sizeof(NTFS_BOOT_SECTOR), bs)
|
|
== sizeof(NTFS_BOOT_SECTOR)) {
|
|
serial = le64_to_cpu(bs->volume_serial_number);
|
|
if (trace > 2)
|
|
safe_fprintf(stderr,"serial 0x%llx\n",serial);
|
|
}
|
|
free(bs);
|
|
}
|
|
|
|
pcrtime->QuadPart = le64_to_cpu(ni->creation_time);
|
|
*plablth = strlen(vol->vol_name)*2;
|
|
ntfs_toutf16(plabel,vol->vol_name,strlen(vol->vol_name));
|
|
/* low-order part of serial number in cpu endianness */
|
|
*pserial = (ULONG)serial;
|
|
*psupobj = 1;
|
|
if (trace > 2)
|
|
safe_fprintf(stderr,"vol name %s\n",vol->vol_name);
|
|
r = 0;
|
|
} else {
|
|
r = -1;
|
|
errno = EINVAL;
|
|
}
|
|
return (r);
|
|
}
|
|
|
|
int ntfs_fill_vol_flags(HANDLE h, ULONG *flags)
|
|
{
|
|
ntfs_volume *vol;
|
|
int r;
|
|
|
|
if (h) {
|
|
vol = (ntfs_volume*)h;
|
|
*flags = le16_to_cpu(vol->flags);
|
|
r = 0;
|
|
} else {
|
|
r = -1;
|
|
errno = EINVAL;
|
|
}
|
|
return (r);
|
|
}
|
|
|
|
int ntfs_fill_ntfs_data(HANDLE h, LARGE_INTEGER *data1,
|
|
ULONG *data2, LARGE_INTEGER *data3)
|
|
{
|
|
int r;
|
|
/*
|
|
struct _NTFS_DATA {
|
|
LARGE_INTEGER VolumeSerialNumber;
|
|
LARGE_INTEGER NumberSectors;
|
|
LARGE_INTEGER TotalClusters;
|
|
LARGE_INTEGER FreeClusters;
|
|
LARGE_INTEGER TotalReserved;
|
|
ULONG BytesPerSector;
|
|
ULONG BytesPerCluster;
|
|
ULONG BytesPerFileRecordSegment;
|
|
ULONG ClustersPerFileRecordSegment;
|
|
LARGE_INTEGER MftValidDataLength;
|
|
LARGE_INTEGER MftStartLcn;
|
|
LARGE_INTEGER Mft2StartLcn;
|
|
LARGE_INTEGER MftZoneStart;
|
|
LARGE_INTEGER MftZoneEnd;
|
|
} ;
|
|
*/
|
|
NTFS_BOOT_SECTOR *bs;
|
|
ntfs_volume *vol;
|
|
runlist_element *rl;
|
|
LONGLONG mft_size;
|
|
char *p;
|
|
|
|
if (h && data1 && data2 && data3) {
|
|
vol = (ntfs_volume*)h;
|
|
bs = (NTFS_BOOT_SECTOR*)ntfs_malloc(sizeof(NTFS_BOOT_SECTOR));
|
|
if (bs) {
|
|
if (ntfs_pread(vol->dev, 0, sizeof(NTFS_BOOT_SECTOR), bs)
|
|
== sizeof(NTFS_BOOT_SECTOR)) {
|
|
data1[0].QuadPart
|
|
= le64_to_cpu(bs->volume_serial_number);
|
|
data1[1].QuadPart
|
|
= le64_to_cpu(bs->number_of_sectors);
|
|
data1[2].QuadPart
|
|
= data1[1].QuadPart
|
|
/ bs->bpb.sectors_per_cluster;
|
|
// FreeClusters not known yet
|
|
data1[3].QuadPart = 0;
|
|
/* this is badly aligned */
|
|
p = (char*)&bs->bpb.reserved_sectors;
|
|
data1[4].QuadPart
|
|
= (p[0] & 255) + ((p[1] & 255) << 8);
|
|
data2[0] = vol->sector_size;
|
|
data2[1] = vol->sector_size
|
|
* bs->bpb.sectors_per_cluster;
|
|
data2[2] = vol->mft_record_size;
|
|
/* beware : the following may be negative */
|
|
data2[3] = bs->clusters_per_mft_record;
|
|
data3[0].QuadPart
|
|
= vol->mft_na->allocated_size;
|
|
data3[1].QuadPart
|
|
= le64_to_cpu(bs->mft_lcn);
|
|
data3[2].QuadPart
|
|
= le64_to_cpu(bs->mftmirr_lcn);
|
|
/*
|
|
* in boot sector returned by Windows
|
|
* zonestart zoneend mftstart mftend zonestart zoneend
|
|
* 0 0x1eb3 0x20 0x1a2d 0x1a20 0x1ec0
|
|
* 0 0x1de20 0x4 0x6bf 0x6a0 0xc820
|
|
*
|
|
* Windows probably replies :
|
|
* MftZoneStart : last MFT cluster used rounded down mod 32
|
|
* MftZoneEnd : min (200MiB beyond start, zoneend) rounded up mod 32
|
|
*/
|
|
rl = vol->mft_na->rl;
|
|
while (rl && rl[0].length && rl[1].length)
|
|
rl++;
|
|
if (rl && rl[0].length) {
|
|
data3[3].QuadPart
|
|
= (rl->lcn + rl->length) & -32;
|
|
mft_size = 209715200 >> vol->cluster_size_bits;
|
|
if (mft_size > (vol->nr_clusters >> 3))
|
|
mft_size = vol->nr_clusters >> 3;
|
|
data3[4].QuadPart
|
|
= ((rl->lcn + rl->length
|
|
+ mft_size - 1) | 31) + 1;
|
|
if (data3[4].QuadPart > vol->nr_clusters)
|
|
data3[4].QuadPart
|
|
= vol->nr_clusters;
|
|
if (trace > 2)
|
|
safe_fprintf(stderr,"mft zone start 0x%llx end 0x%llx\n",
|
|
(long long)data3[3].QuadPart,
|
|
(long long)data3[4].QuadPart);
|
|
}
|
|
}
|
|
free(bs);
|
|
}
|
|
if (trace > 2)
|
|
safe_fprintf(stderr,"vol name %s\n",vol->vol_name);
|
|
r = 0;
|
|
} else {
|
|
r = -1;
|
|
errno = EINVAL;
|
|
}
|
|
return (r);
|
|
}
|
|
|
|
int ntfs_fill_bitmap(HANDLE h, PVOID buf, ULONG buflen, ULONGLONG pos,
|
|
ULONGLONG *pfullsize, ULONG *pretlen)
|
|
{
|
|
int r;
|
|
ntfs_volume *vol;
|
|
ntfs_attr *bmp_na;
|
|
SLONGLONG got;
|
|
ULONGLONG size;
|
|
|
|
*pretlen = 0;
|
|
if (h && buf && buflen && pretlen) {
|
|
vol = (ntfs_volume*)h;
|
|
bmp_na = vol->lcnbmp_na;
|
|
*pfullsize = vol->nr_clusters;
|
|
if (pos < (ULONGLONG)bmp_na->data_size) {
|
|
size = bmp_na->data_size - pos;
|
|
if (size > buflen)
|
|
size = buflen;
|
|
got = ntfs_attr_pread(bmp_na, pos, size, buf);
|
|
if (trace > 2)
|
|
safe_fprintf(stderr,"Read bitmap pos %lld of %lld size %ld got %ld\n",(long long)pos,(long long)bmp_na->data_size,(long)size,(long)got);
|
|
if (got <= 0)
|
|
r = -1;
|
|
else {
|
|
*pretlen = got;
|
|
r = 0;
|
|
}
|
|
} else
|
|
r = -1; /* beyond the end */
|
|
} else {
|
|
r = -1;
|
|
errno = EINVAL;
|
|
}
|
|
return (r);
|
|
}
|
|
|
|
int ntfs_fill_inode_data(HANDLE h, PVOID buf, ULONG buflen, ULONGLONG mref,
|
|
ULONG *pretlen)
|
|
{
|
|
int r;
|
|
int count;
|
|
ntfs_volume *vol;
|
|
|
|
r = -1;
|
|
if (h && buf && buflen) {
|
|
vol = (ntfs_volume*)h;
|
|
/* read full MFT records */
|
|
count = buflen/vol->mft_record_size;
|
|
if (count) {
|
|
r = ntfs_mft_records_read(vol, mref, count, buf);
|
|
if (!r)
|
|
*pretlen = count*vol->mft_record_size;
|
|
if (trace > 2)
|
|
safe_fprintf(stderr,"getting %d MFT records, r %d retlen %ld\n",count,r,(long)*pretlen);
|
|
}
|
|
} else {
|
|
errno = EINVAL;
|
|
}
|
|
return (r);
|
|
}
|
|
|
|
/*
|
|
* Get a (partial) runlist
|
|
*/
|
|
|
|
void showrl(FILE *f, const char *text, const runlist_element *rl, const runlist_element *cur)
|
|
{
|
|
int k;
|
|
|
|
if (cur && rl)
|
|
safe_fprintf(f," runlist at \"%s\" 0x%lx cur at index %d\n",text,(long)rl,(int)(cur-rl));
|
|
else
|
|
safe_fprintf(f," runlist at \"%s\" 0x%lx\n",text,(long)rl);
|
|
if (rl)
|
|
{
|
|
k = 0;
|
|
do {
|
|
safe_fprintf(f," %d vcn 0x%04llx lcn 0x%016llx length 0x%lx\n",k,(long long)rl[k].vcn,(long long)rl[k].lcn,(long)rl[k].length);
|
|
} while ((k < 600) && rl[k++].length);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Returns 0 if successful and the end is reached
|
|
* 1 if successful and the end is not reached
|
|
* -1 if there was an error
|
|
*/
|
|
|
|
int ntfs_fill_runlist(HANDLE h, ULONGLONG vcn, ULONG *pcnt,
|
|
PVOID buf, ULONG size)
|
|
{
|
|
ntfs_attr *na;
|
|
ntfs_inode *ni;
|
|
runlist_element *rl;
|
|
ULONGLONG *data;
|
|
unsigned long i;
|
|
unsigned long rlcnt;
|
|
unsigned long first;
|
|
unsigned long outcnt;
|
|
int r;
|
|
|
|
r = -1;
|
|
na = (ntfs_attr*)h;
|
|
if (na && na->ni && na->ni->vol && buf) {
|
|
ni = na->ni;
|
|
if (trace > 2)
|
|
safe_fprintf(stderr,"Getting runlist for inode %lld\n",(long long)ni->mft_no);
|
|
if (!ntfs_attr_map_whole_runlist(na)) {
|
|
rlcnt = 0;
|
|
rl = na->rl;
|
|
while (rl[rlcnt].length)
|
|
rlcnt++;
|
|
first = 0;
|
|
while (rl[first].length && ((ULONGLONG)rl[first].vcn < vcn))
|
|
first++;
|
|
if ((ULONGLONG)rl[first].vcn == vcn) {
|
|
outcnt = rlcnt - first;
|
|
r = 0;
|
|
if (outcnt*16 > size) {
|
|
outcnt = size >> 4;
|
|
r = 1;
|
|
}
|
|
*pcnt = outcnt;
|
|
data = (ULONGLONG*)buf;
|
|
data[1] = rl[first].lcn;
|
|
for (i=1; i<outcnt; i++) {
|
|
data[2*i - 2] = rl[i + first].vcn;
|
|
data[2*i + 1] = rl[i + first].lcn;
|
|
}
|
|
data[2*outcnt - 2] = rl[outcnt + first].vcn;
|
|
} else {
|
|
if (trace > 2)
|
|
safe_fprintf(stderr,"** Runlist request not at beginning of run\n");
|
|
}
|
|
}
|
|
}
|
|
return (r);
|
|
}
|
|
|
|
/*
|
|
* Basic allocation of consecutive clusters
|
|
* The designated clusters are supposed to be unallocated
|
|
*
|
|
* Returns zero if successful
|
|
*/
|
|
|
|
int ntfs_cluster_alloc_basic(ntfs_volume *vol, s64 lcn, s64 count)
|
|
{
|
|
int ret = -1;
|
|
runlist_element *rl;
|
|
|
|
/* this makes sure the requested zone is free */
|
|
rl = ntfs_cluster_alloc(vol, 0, count, lcn, DATA_ZONE);
|
|
if (rl && !rl->vcn && (rl->lcn == lcn)
|
|
&& (rl->length == count) && !rl[1].length) {
|
|
free(rl);
|
|
ret = 0;
|
|
} else {
|
|
if (trace > 2)
|
|
safe_fprintf(stderr,"** Failed to allocate %lld clusters from 0x%llx\n",
|
|
(long long)count,(long long)lcn);
|
|
if (rl)
|
|
ntfs_cluster_free_from_rl(vol, rl);
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
/*
|
|
* Copy data from consecutive clusters
|
|
*
|
|
* Returns zero if successful
|
|
*/
|
|
|
|
static int copy_clusters(ntfs_volume *vol, LONGLONG target,
|
|
LONGLONG source, ULONG cnt)
|
|
{
|
|
char * buf;
|
|
int r;
|
|
ULONGLONG bytesrc;
|
|
ULONGLONG bytetgt;
|
|
ULONGLONG size;
|
|
unsigned long bufsz;
|
|
unsigned long chunk;
|
|
|
|
r = 0;
|
|
bytesrc = source << vol->cluster_size_bits;
|
|
bytetgt = target << vol->cluster_size_bits;
|
|
size = ((ULONGLONG)cnt) << vol->cluster_size_bits;
|
|
bufsz = (size < 32768 ? size : 32768);
|
|
buf = (char*)ntfs_malloc(bufsz);
|
|
if (buf) {
|
|
do {
|
|
chunk = (size < bufsz ? size : bufsz);
|
|
if (trace > 2)
|
|
safe_fprintf(stderr,"copying 0x%llx to 0x%llx chunk %ld\n",(long long)bytesrc,(long long)bytetgt,(long)chunk);
|
|
if ((ntfs_pread(vol->dev, bytesrc, chunk, buf) == (long)chunk)
|
|
&& (ntfs_pwrite(vol->dev, bytetgt, chunk, buf) == (long)chunk)) {
|
|
bytesrc += chunk;
|
|
bytetgt += chunk;
|
|
size -= chunk;
|
|
} else
|
|
r = -1;
|
|
} while (size && !r);
|
|
free(buf);
|
|
}
|
|
return (r);
|
|
}
|
|
|
|
/*
|
|
* Merge a run with adjacent runs, if possible
|
|
*
|
|
* Returns zero if successful or unneeded
|
|
*
|
|
* Note : this is supposed to restore original runlist if merging fails
|
|
*/
|
|
|
|
static int merge_runs(LONGLONG svcn, LONGLONG tlcn, ULONG cnt,
|
|
runlist_element *rl, unsigned long moved)
|
|
{
|
|
int r;
|
|
|
|
r = 0;
|
|
if (svcn && ((rl[moved-1].lcn + rl[moved-1].length) == tlcn)) {
|
|
/* merge with previous */
|
|
if (trace > 2)
|
|
safe_fprintf(stderr,"merging with previous\n");
|
|
rl[moved-1].length += cnt;
|
|
do {
|
|
rl[moved] = rl[moved+1];
|
|
moved++;
|
|
} while (rl[moved].length);
|
|
} else {
|
|
if (rl[moved+1].length
|
|
&& ((tlcn + cnt) == rl[moved+1].lcn)) {
|
|
if (trace > 2)
|
|
safe_fprintf(stderr,"merging with next\n");
|
|
/* merge with next */
|
|
rl[moved].lcn = tlcn;
|
|
rl[moved].length = rl[moved+1].length + cnt;
|
|
moved++;
|
|
do {
|
|
rl[moved] = rl[moved+1];
|
|
moved++;
|
|
} while (rl[moved].length);
|
|
} else {
|
|
/* cannot merge */
|
|
rl[moved].lcn = tlcn;
|
|
}
|
|
}
|
|
return (r);
|
|
}
|
|
|
|
int ntfs_actual_move_clusters(ntfs_attr *na, runlist_element *rl,
|
|
unsigned long moving, LONGLONG tlcn, ULONG cnt)
|
|
{
|
|
ntfs_volume *vol;
|
|
LONGLONG slcn;
|
|
LONGLONG svcn;
|
|
int r;
|
|
|
|
if (trace > 2)
|
|
safe_fprintf(stderr,"inode %lld move vcn 0x%llx lth %ld slcn 0x%llx tlcn 0x%llx\n",
|
|
(long long)na->ni->mft_no,
|
|
(long long)rl[moving].vcn,(long)cnt,
|
|
(long long)rl[moving].lcn,(long long)tlcn);
|
|
r = -1;
|
|
vol = na->ni->vol;
|
|
slcn = rl[moving].lcn;
|
|
svcn = rl[moving].vcn;
|
|
/* copy clusters */
|
|
if (!copy_clusters(vol,tlcn,slcn,cnt)
|
|
/* build the new runlist */
|
|
&& !merge_runs(svcn, tlcn, cnt, rl, moving)) {
|
|
/*
|
|
* This is where we enter the dangerous zone
|
|
* currently we cannot roll back if something fails
|
|
*/
|
|
/* update the runlist */
|
|
if (!ntfs_attr_update_mapping_pairs(na,svcn)
|
|
/* and free unused clusters */
|
|
&& !ntfs_cluster_free_basic(vol,slcn,cnt)) {
|
|
r = 0;
|
|
} else {
|
|
if (trace > 2)
|
|
safe_fprintf(stderr,"** Severe failure while updating the runlist\n");
|
|
}
|
|
} else {
|
|
/* copying or merging failed, just free target */
|
|
if (ntfs_cluster_free_basic(vol,tlcn,cnt)) {
|
|
if (trace > 2)
|
|
safe_fprintf(stderr,"** Could not roll back after failed cluster copy\n");
|
|
} else {
|
|
if (trace > 2)
|
|
safe_fprintf(stderr,"** Rolled back after failed cluster copy\n");
|
|
}
|
|
}
|
|
if (r && (trace > 2)) {
|
|
safe_fprintf(stderr,"** Updated inode %lld slcn 0x%llx tlcn 0x%llx cnt %ld\n",
|
|
(long long)na->ni->mft_no,(long long)slcn,
|
|
(long long)tlcn, (long)cnt);
|
|
safe_fprintf(stderr,"** The above data may be useful for repairing !\n");
|
|
}
|
|
return (r);
|
|
}
|
|
|
|
static ULONG ntfs_partial_move_clusters(runlist_element *rl,
|
|
LONGLONG tlcn, ULONG cnt,
|
|
ntfs_attr *na, unsigned long moving)
|
|
{
|
|
unsigned long last;
|
|
ULONG cando;
|
|
ULONG r;
|
|
|
|
r = 0;
|
|
/* have to split the run */
|
|
if (rl[moving].length > cnt) {
|
|
if (trace > 2)
|
|
safe_fprintf(stderr,"must split, length %ld cnt %ld\n",
|
|
(long)rl[moving].length,(long)cnt);
|
|
/* split the run if not fully moved */
|
|
rl = ntfs_rl_extend(na, rl, 1);
|
|
if (rl) {
|
|
last = moving;
|
|
while (rl[last].length)
|
|
last++;
|
|
while (last > moving) {
|
|
rl[last + 1] = rl[last];
|
|
last--;
|
|
}
|
|
rl[moving + 1].lcn = rl[moving].lcn + cnt;
|
|
rl[moving + 1].vcn = rl[moving].vcn + cnt;
|
|
rl[moving + 1].length = rl[moving].length - cnt;
|
|
rl[moving].length = cnt;
|
|
} else {
|
|
if (trace > 2)
|
|
safe_fprintf(stderr,"** Failed to split a run\n");
|
|
}
|
|
}
|
|
if (rl && (rl[moving].length < cnt))
|
|
cando = rl[moving].length;
|
|
else
|
|
cando = cnt;
|
|
|
|
if (rl
|
|
&& cando
|
|
&& !ntfs_actual_move_clusters(na, rl, moving, tlcn, cando))
|
|
r = cando;
|
|
return (r);
|
|
}
|
|
|
|
|
|
/*
|
|
* Move clusters
|
|
*
|
|
* Returns zero if successful or unneeded
|
|
*/
|
|
|
|
int ntfs_move_clusters(HANDLE h, LONGLONG svcn, LONGLONG tlcn, ULONG cnt)
|
|
{
|
|
ntfs_attr *na;
|
|
ntfs_inode *ni;
|
|
runlist_element *rl;
|
|
unsigned long moving;
|
|
ULONG moved;
|
|
ULONG done;
|
|
int r;
|
|
|
|
r = -1;
|
|
na = (ntfs_attr*)h;
|
|
if ((trace > 2) && na && na->ni)
|
|
safe_fprintf(stderr,"moving clusters inode %lld vcn 0x%llx cnt %ld to lcn 0x%llx\n",(long long)na->ni->mft_no,(long long)svcn,(long)cnt,(long long)tlcn);
|
|
/* first check we can allocate target */
|
|
if (na
|
|
&& na->ni
|
|
&& na->ni->vol
|
|
&& !ntfs_cluster_alloc_basic(na->ni->vol,tlcn,cnt)) {
|
|
ni = na->ni;
|
|
/* the runlist up to the end is needed for updating */
|
|
if (!ntfs_attr_map_whole_runlist(na)) {
|
|
if (trace > 2)
|
|
showrl(stderr,"original",na->rl,NULL);
|
|
done = 0;
|
|
do {
|
|
rl = na->rl;
|
|
moving = 0;
|
|
while (rl[moving].length
|
|
&& (rl[moving].vcn < (svcn + done)))
|
|
moving++;
|
|
if (trace > 2)
|
|
safe_fprintf(stderr,"moving %ld lth %ld vcn %lld svcn %lld\n",(long)moving,(long)rl[moving].length,rl[moving].vcn,svcn+done);
|
|
moved = 0;
|
|
if (rl[moving].length
|
|
&& (rl[moving].vcn == (svcn + done))) {
|
|
moved = ntfs_partial_move_clusters(rl,
|
|
tlcn + done, cnt - done,
|
|
na, moving);
|
|
if (moved)
|
|
done += moved;
|
|
}
|
|
} while (moved && (done < cnt));
|
|
if (done < cnt) {
|
|
/* failed : free the unused clusters */
|
|
ntfs_bitmap_clear_run(ni->vol->lcnbmp_na,
|
|
tlcn + done, cnt - done);
|
|
if (trace > 2) {
|
|
safe_fprintf(stderr,"** Could not find the run to move\n");
|
|
safe_fprintf(stderr,"svcn 0x%llx tlcn 0x%llx count 0x%lx moving %ld\n",(long long)svcn,(long long)tlcn,(long)cnt,(long)moving);
|
|
}
|
|
}
|
|
if (trace > 2)
|
|
showrl(stderr,"final",na->rl,NULL);
|
|
if (done == cnt)
|
|
r = 0;
|
|
} else {
|
|
ntfs_bitmap_clear_run(ni->vol->lcnbmp_na, tlcn, cnt);
|
|
if (trace > 2)
|
|
safe_fprintf(stderr,"** Could not map the runlist\n");
|
|
}
|
|
}
|
|
return (r);
|
|
}
|