Commit 020d7c23 authored by Zach C's avatar Zach C Committed by Matthew Colyer

Adds seeking, directory creation and improves file writing. Also various other cleanups.

Implements creating directories as well as writing and deleting files in iFuse.
Signed-off-by: 's avatarMatthew Colyer <mcolyer@mcolyer-laptop.(none)>
parent 4c3f8649
......@@ -29,13 +29,15 @@ extern int debug;
/* Locking, for thread-safety (well... kind of, hehe) */
void afc_lock(AFClient *client) {
if (debug) printf("In the midst of a lock...\n");
while (client->lock) {
sleep(200);
usleep(500); // they say it's obsolete, but whatever
}
client->lock = 1;
}
void afc_unlock(AFClient *client) { // just to be pretty
if (debug) printf("Unlock!\n");
client->lock = 0;
}
......@@ -102,13 +104,13 @@ int dispatch_AFC_packet(AFClient *client, const char *data, int length) {
if ((length) < (client->afc_packet->entire_length - client->afc_packet->this_length)) {
if (debug) printf("dispatch_AFC_packet: Length did not resemble what it was supposed to based on the packet.\nlength minus offset: %i\nrest of packet: %i\n", length-offset, client->afc_packet->entire_length - client->afc_packet->this_length);
free(buffer);
return 0;
return -1;
}
if (debug) printf("dispatch_AFC_packet: fucked-up packet method (probably a write)\n");
memcpy(buffer+sizeof(AFCPacket), data, offset);
bytes = mux_send(client->connection, buffer, client->afc_packet->this_length);
free(buffer);
if (bytes <= 0) { return 0; }
if (bytes <= 0) { return bytes; }
if (debug) {
printf("dispatch_AFC_packet: sent the first now go with the second\n");
printf("Length: %i\n", length-offset);
......@@ -118,8 +120,7 @@ int dispatch_AFC_packet(AFClient *client, const char *data, int length) {
bytes = mux_send(client->connection, data+offset, length-offset);
if (bytes <= 0) { return 0; }
else { return bytes; }
return bytes;
} else {
if (debug) printf("dispatch_AFC_packet doin things the old way\n");
char *buffer = (char*)malloc(sizeof(char) * client->afc_packet->this_length);
......@@ -130,10 +131,9 @@ int dispatch_AFC_packet(AFClient *client, const char *data, int length) {
if (debug) fwrite(buffer, 1, client->afc_packet->this_length, stdout);
if (debug) printf("\n");
bytes = mux_send(client->connection, buffer, client->afc_packet->this_length);
if (bytes <= 0) return 0;
else return bytes;
return bytes;
}
return 0;
return -1;
}
int receive_AFC_data(AFClient *client, char **dump_here) {
......@@ -148,7 +148,7 @@ int receive_AFC_data(AFClient *client, char **dump_here) {
free(buffer);
printf("Just didn't get enough.\n");
*dump_here = NULL;
return 0;
return -1;
}
r_packet = (AFCPacket*)malloc(sizeof(AFCPacket));
......@@ -177,19 +177,19 @@ int receive_AFC_data(AFClient *client, char **dump_here) {
if(param1 == 0) {
if (debug) printf("... false alarm, but still\n");
*dump_here = NULL;
return 1;
return 0;
}
else { if (debug) printf("Errno %i\n", param1); }
free(r_packet);
*dump_here = NULL;
return 0;
return -1;
} else {
if (debug) printf("Operation code %x\nFull length %i and this length %i\n", r_packet->operation, r_packet->entire_length, r_packet->this_length);
}
recv_len = r_packet->entire_length - r_packet->this_length;
free(r_packet);
if (!recv_len)
if (!recv_len && r_packet->operation == AFC_SUCCESS_RESPONSE)
{
*dump_here = NULL;
return 0;
......@@ -240,10 +240,28 @@ char **afc_get_dir_list(AFClient *client, const char *dir) {
char *blah = NULL, **list = NULL;
client->afc_packet->entire_length = client->afc_packet->this_length = 0;
bytes = dispatch_AFC_packet(client, dir, strlen(dir));
if (!bytes) { afc_unlock(client); return NULL; }
if (bytes <= 0) { afc_unlock(client); return NULL; }
bytes = receive_AFC_data(client, &blah);
if (bytes < 0 && !blah) { afc_unlock(client); return NULL; }
list = make_strings_list(blah, bytes);
free(blah);
afc_unlock(client);
return list;
}
char **afc_get_devinfo(AFClient *client) {
afc_lock(client);
client->afc_packet->operation = AFC_GET_DEVINFO;
int bytes = 0;
char *blah = NULL, **list = NULL;
client->afc_packet->entire_length = client->afc_packet->this_length = 0;
bytes = dispatch_AFC_packet(client, NULL, 0);
if (bytes < 0) { afc_unlock(client); return NULL; }
bytes = receive_AFC_data(client, &blah);
if (!bytes && !blah) { afc_unlock(client); return NULL; }
if (bytes < 0 && !blah) { afc_unlock(client); return NULL; }
list = make_strings_list(blah, bytes);
free(blah);
......@@ -251,6 +269,7 @@ char **afc_get_dir_list(AFClient *client, const char *dir) {
return list;
}
char **make_strings_list(char *tokens, int true_length) {
if (!tokens || !true_length) return NULL;
int nulls = 0, i = 0, j = 0;
......@@ -279,7 +298,7 @@ int afc_delete_file(AFClient *client, const char *path) {
bytes = receive_AFC_data(client, &receive);
free(receive);
afc_unlock(client);
if (bytes <= 0) { return 0; }
if (bytes < 0) { return 0; }
else return 1;
}
......@@ -302,12 +321,28 @@ int afc_rename_file(AFClient *client, const char *from, const char *to) {
bytes = receive_AFC_data(client, &receive);
free(receive);
afc_unlock(client);
if (bytes <= 0) return 0;
if (bytes < 0) return 0;
else return 1;
}
int afc_mkdir(AFClient *client, const char *dir) {
if (!client) return 0;
afc_lock(client);
int bytes = 0;
char *recvd = NULL;
client->afc_packet->operation = AFC_MAKE_DIR;
client->afc_packet->this_length = client->afc_packet->entire_length = 0;
bytes = dispatch_AFC_packet(client, dir, strlen(dir));
if (bytes <= 0) { afc_unlock(client); return 0; }
bytes = receive_AFC_data(client, &recvd);
afc_unlock(client);
if (recvd) { free(recvd); recvd = NULL; }
if (bytes == 0) return 1;
else return 0;
}
AFCFile *afc_get_file_info(AFClient *client, const char *path) {
client->afc_packet->operation = AFC_GET_INFO;
client->afc_packet->entire_length = client->afc_packet->this_length = 0;
......@@ -350,7 +385,7 @@ AFCFile *afc_get_file_info(AFClient *client, const char *path) {
}
AFCFile *afc_open_file(AFClient *client, const char *filename, uint32 file_mode) {
if (file_mode != AFC_FILE_READ && file_mode != AFC_FILE_WRITE) return NULL;
//if (file_mode != AFC_FILE_READ && file_mode != AFC_FILE_WRITE) return NULL;
if (!client ||!client->connection || !client->afc_packet) return NULL;
afc_lock(client);
char *further_data = (char*)malloc(sizeof(char) * (8 + strlen(filename) + 1));
......@@ -372,17 +407,19 @@ AFCFile *afc_open_file(AFClient *client, const char *filename, uint32 file_mode)
return NULL;
} else {
length_thing = receive_AFC_data(client, &further_data);
if (length_thing && further_data) {
if (length_thing > 0 && further_data) {
afc_unlock(client); // don't want to hang on the next call... and besides, it'll re-lock, do its thing, and unlock again anyway.
file_infos = afc_get_file_info(client, filename);
memcpy(&file_infos->filehandle, further_data, 4);
return file_infos;
} else {
if (debug) printf("didn't get further data or something\n");
afc_unlock(client);
return NULL;
}
}
if (debug) printf("what the fuck\n");
afc_unlock(client);
return NULL;
}
......@@ -408,7 +445,8 @@ int afc_read_file(AFClient *client, AFCFile *file, char *data, int length) {
// Looping here to get around the maximum amount of data that recieve_AFC_data can handle
while (current_count < length){
if (debug) printf("afc_read_file: current count is %i but length is %i\n", current_count, length);
// Send the read command
AFCFilePacket *packet = (AFCFilePacket*)malloc(sizeof(AFCFilePacket));
packet->unknown1 = packet->unknown2 = 0;
......@@ -421,6 +459,7 @@ int afc_read_file(AFClient *client, AFCFile *file, char *data, int length) {
// If we get a positive reponse to the command gather the data
if (bytes > 0) {
bytes = receive_AFC_data(client, &input);
if (debug) printf("afc_read_file: bytes returned: %i\n", bytes);
if (bytes < 0) {
if (input) free(input);
afc_unlock(client);
......@@ -431,7 +470,7 @@ int afc_read_file(AFClient *client, AFCFile *file, char *data, int length) {
return current_count;
} else {
if (input) {
printf("afc_read_file: %d\n", bytes);
if (debug) printf("afc_read_file: %d\n", bytes);
memcpy(data+current_count, input, (bytes > length) ? length : bytes);
free(input);
input = NULL;
......@@ -444,6 +483,7 @@ int afc_read_file(AFClient *client, AFCFile *file, char *data, int length) {
}
}
afc_unlock(client);
if (debug) printf("afc_read_file: returning current_count as %i\n", current_count);
return current_count;
}
......@@ -451,29 +491,53 @@ int afc_write_file(AFClient *client, AFCFile *file, const char *data, int length
char *acknowledgement = NULL;
if (!client ||!client->afc_packet || !client->connection || !file) return -1;
afc_lock(client);
client->afc_packet->this_length = sizeof(AFCPacket) + 8;
client->afc_packet->entire_length = client->afc_packet->this_length + length;
client->afc_packet->operation = AFC_WRITE;
if (debug) printf("afc_write_file: Write length: %i\n", length);
uint32 zero = 0, bytes = 0;
const int MAXIMUM_WRITE_SIZE = 1 << 16;
uint32 zero = 0, bytes = 0, segments = (length / MAXIMUM_WRITE_SIZE), current_count = 0, i = 0;
char *out_buffer = NULL;
for (i = 0; i < segments; i++) { // Essentially, yeah, divide it into segments.
client->afc_packet->this_length = sizeof(AFCPacket) + 8;
//client->afc_packet->entire_length = client->afc_packet->this_length + length;
client->afc_packet->entire_length = client->afc_packet->this_length + MAXIMUM_WRITE_SIZE;
client->afc_packet->operation = AFC_WRITE;
out_buffer = (char*)malloc(sizeof(char) * client->afc_packet->entire_length - sizeof(AFCPacket));
memcpy(out_buffer, (char*)&file->filehandle, sizeof(uint32));
memcpy(out_buffer+4, (char*)&zero, sizeof(uint32));
memcpy(out_buffer+8, data+current_count, MAXIMUM_WRITE_SIZE);
bytes = dispatch_AFC_packet(client, out_buffer, MAXIMUM_WRITE_SIZE + 8);
if (bytes < 0) { afc_unlock(client); return bytes; }
free(out_buffer); out_buffer = NULL; // cleanup and hope it works
current_count += bytes;
bytes = receive_AFC_data(client, &acknowledgement);
if (bytes < 0) { afc_unlock(client); return current_count; }
}
// By this point, we should be at the end. i.e. the last segment that didn't get sent in the for loop
// this length is fine because it's always sizeof(AFCPacket) + 8, but to be sure we do it again
if (current_count == length) { afc_unlock(client); return current_count; }
client->afc_packet->this_length = sizeof(AFCPacket) + 8;
client->afc_packet->entire_length = client->afc_packet->this_length + (length - current_count);
// operation is already AFC_WRITE, but set again to be sure
client->afc_packet->operation = AFC_WRITE;
out_buffer = (char*)malloc(sizeof(char) * client->afc_packet->entire_length - sizeof(AFCPacket));
memcpy(out_buffer, (char*)&file->filehandle, sizeof(uint32));
memcpy(out_buffer+4, (char*)&zero, sizeof(uint32));
memcpy(out_buffer+8, data, length);
bytes = dispatch_AFC_packet(client, out_buffer, length + 8);
if (!bytes) return -1;
memcpy(out_buffer+8, data+current_count, (length - current_count));
bytes = dispatch_AFC_packet(client, out_buffer, (length - current_count) + 8);
free(out_buffer); out_buffer = NULL;
current_count += bytes;
if (bytes <= 0) { afc_unlock(client); return current_count; }
zero = bytes;
bytes = receive_AFC_data(client, &acknowledgement);
afc_unlock(client);
if (bytes <= 0) {
if (bytes < 0) {
if (debug) printf("afc_write_file: uh oh?\n");
}
return zero;
return current_count;
}
void afc_close_file(AFClient *client, AFCFile *file) {
......@@ -490,9 +554,61 @@ void afc_close_file(AFClient *client, AFCFile *file) {
free(buffer);
client->afc_packet->entire_length = client->afc_packet->this_length = 0;
if (!bytes) return;
if (bytes <= 0) { afc_unlock(client); return; }
bytes = receive_AFC_data(client, &buffer);
afc_unlock(client);
if (buffer) { free(buffer); }
return;
}
int afc_seek_file(AFClient *client, AFCFile *file, int seekpos) {
afc_lock(client);
char *buffer = (char*)malloc(sizeof(char) * 24);
uint32 seekto = 0, bytes = 0, zero = 0;
if (seekpos < 0) seekpos = file->size - abs(seekpos);
seekto = seekpos;
memcpy(buffer, &file->filehandle, sizeof(uint32)); // handle
memcpy(buffer+4, &zero, sizeof(uint32)); // pad
memcpy(buffer+8, &zero, sizeof(uint32)); // fromwhere
memcpy(buffer+12, &zero, sizeof(uint32)); // pad
memcpy(buffer+16, &seekto, sizeof(uint32)); // offset
memcpy(buffer+20, &zero, sizeof(uint32)); // pad
client->afc_packet->operation = AFC_FILE_SEEK;
client->afc_packet->this_length = client->afc_packet->entire_length = 0;
bytes = dispatch_AFC_packet(client, buffer, 23);
free(buffer); buffer = NULL;
if (bytes <= 0) { afc_unlock(client); return -1; }
bytes = receive_AFC_data(client, &buffer);
if (buffer) free(buffer);
afc_unlock(client);
if (bytes >= 0) return 0;
else return -1;
}
int afc_truncate_file(AFClient *client, AFCFile *file, uint32 newsize) {
afc_lock(client);
char *buffer = (char*)malloc(sizeof(char) * 16);
uint32 bytes = 0, zero = 0;
memcpy(buffer, &file->filehandle, sizeof(uint32)); // handle
memcpy(buffer+4, &zero, sizeof(uint32)); // pad
memcpy(buffer+8, &newsize, sizeof(uint32)); // newsize
memcpy(buffer+12, &zero, 3); // pad
client->afc_packet->operation = AFC_FILE_TRUNCATE;
client->afc_packet->this_length = client->afc_packet->entire_length = 0;
bytes = dispatch_AFC_packet(client, buffer, 15);
free(buffer); buffer = NULL;
if (bytes <= 0) { afc_unlock(client); return -1; }
bytes = receive_AFC_data(client, &buffer);
if (buffer) free(buffer);
afc_unlock(client);
if (bytes >= 0) return 0;
else return -1;
}
......@@ -56,8 +56,14 @@ typedef struct __AFCToken {
enum {
AFC_FILE_READ = 0x00000002,
AFC_FILE_WRITE = 0x00000003
AFC_FILE_READ = 0x00000002, // seems to be able to read and write files
AFC_FILE_WRITE = 0x00000003, // writes and creates a file, blanks it out, etc.
AFC_FILE_RW = 0x00000005, // seems to do the same as 2. Might even create the file.
AFC_FILE_OP4 = 0x00000004, // no idea -- appears to be "write" -- clears file beforehand like 3
AFC_FILE_OP6 = 0x00000006, // no idea yet -- appears to be the same as 5.
AFC_FILE_OP1 = 0x00000001, // no idea juuust yet... probably read.
AFC_FILE_OP0 = 0x00000000,
AFC_FILE_OP10 = 0x0000000a
};
enum {
......@@ -65,11 +71,14 @@ enum {
AFC_GET_INFO = 0x0000000a,
AFC_GET_DEVINFO = 0x0000000b,
AFC_LIST_DIR = 0x00000003,
AFC_MAKE_DIR = 0x00000009,
AFC_DELETE = 0x00000008,
AFC_RENAME = 0x00000018,
AFC_SUCCESS_RESPONSE = 0x00000002,
AFC_FILE_OPEN = 0x0000000d,
AFC_FILE_CLOSE = 0x00000014,
AFC_FILE_SEEK = 0x00000011,
AFC_FILE_TRUNCATE = 0x00000015,
AFC_FILE_HANDLE = 0x0000000e,
AFC_READ = 0x0000000f,
AFC_WRITE = 0x00000010
......
......@@ -41,7 +41,7 @@
GHashTable *file_handles;
int fh_index = 0;
int debug = 1;
int debug = 0;
static int ifuse_getattr(const char *path, struct stat *stbuf) {
int res = 0;
......@@ -53,7 +53,8 @@ static int ifuse_getattr(const char *path, struct stat *stbuf) {
if (!file){
res = -ENOENT;
} else {
stbuf->st_mode = file->type | 0444;
//stbuf->st_mode = file->type | 0444; // testing write access too now
stbuf->st_mode = file->type | 0644; // but we don't want anything on the iPhone executable, like, ever
stbuf->st_size = file->size;
//stbuf->st_nlink = 2;
}
......@@ -81,14 +82,34 @@ static int ifuse_readdir(const char *path, void *buf, fuse_fill_dir_t filler,
return 0;
}
static int ifuse_open(const char *path, struct fuse_file_info *fi) {
static int ifuse_create(const char *path, mode_t mode, struct fuse_file_info *fi) {
// exactly the same as open but using a different mode
AFCFile *file;
AFClient *afc = fuse_get_context()->private_data;
file = afc_open_file(afc, path, AFC_FILE_WRITE);
fh_index++;
fi->fh = fh_index;
g_hash_table_insert(file_handles, &fh_index, file);
return 0;
}
if((fi->flags & 3) != O_RDONLY)
return -EACCES;
static int ifuse_open(const char *path, struct fuse_file_info *fi) {
AFCFile *file;
AFClient *afc = fuse_get_context()->private_data;
uint32 mode = 0;
/*if((fi->flags & 3) != O_RDONLY)
return -EACCES;*/ // trying to test write access here
if ((fi->flags & 3) == O_RDWR || (fi->flags & 3) == O_WRONLY) {
mode = AFC_FILE_READ;
} else if ((fi->flags & 3) == O_RDONLY) {
mode = AFC_FILE_READ;
} else {
mode = AFC_FILE_READ;
}
file = afc_open_file(afc, path, AFC_FILE_READ);
file = afc_open_file(afc, path, mode);
fh_index++;
fi->fh = fh_index;
......@@ -111,10 +132,30 @@ static int ifuse_read(const char *path, char *buf, size_t size, off_t offset,
return -ENOENT;
}
bytes = afc_seek_file(afc, file, offset);
bytes = afc_read_file(afc, file, buf, size);
return bytes;
}
static int ifuse_write(const char *path, const char *buf, size_t size, off_t offset, struct fuse_file_info *fi) {
int bytes = 0;
AFCFile *file = NULL;
AFClient *afc = fuse_get_context()->private_data;
if (size == 0) return 0;
file = g_hash_table_lookup(file_handles, &(fi->fh));
if (!file) return -ENOENT;
bytes = afc_seek_file(afc, file, offset);
bytes = afc_write_file(afc, file, buf, size);
return bytes;
}
static int ifuse_fsync(const char *path, int datasync, struct fuse_file_info *fi) {
return 0;
}
static int ifuse_release(const char *path, struct fuse_file_info *fi){
AFCFile *file;
AFClient *afc = fuse_get_context()->private_data;
......@@ -181,11 +222,89 @@ void ifuse_cleanup(void *data) {
}
}
int ifuse_flush(const char *path, struct fuse_file_info *fi) {
return 0;
}
int ifuse_statfs(const char *path, struct statvfs *stats) {
AFClient *afc = fuse_get_context()->private_data;
char **info_raw = afc_get_devinfo(afc);
uint32 totalspace = 0, freespace = 0, blocksize = 0, i = 0;
if (!info_raw) return -ENOENT;
for (i = 0; strcmp(info_raw[i], ""); i++) {
if (!strcmp(info_raw[i], "FSTotalBytes")) {
totalspace = atoi(info_raw[i+1]);
} else if (!strcmp(info_raw[i], "FSFreeBytes")) {
freespace = atoi(info_raw[i+1]);
} else if (!strcmp(info_raw[i], "FSBlockSize")) {
blocksize = atoi(info_raw[i+1]);
}
}
// Now to fill the struct.
stats->f_bsize = stats->f_frsize = blocksize;
stats->f_blocks = totalspace / blocksize; // gets the blocks by dividing bytes by blocksize
stats->f_bfree = stats->f_bavail = freespace / blocksize; // all bytes are free to everyone, I guess.
stats->f_namemax = 255; // blah
stats->f_files = stats->f_ffree = 1000000000; // make up any old thing, I guess
return 0;
}
int ifuse_truncate(const char *path, off_t size) {
int result = 0;
AFClient *afc = fuse_get_context()->private_data;
AFCFile *tfile = afc_open_file(afc, path, AFC_FILE_READ);
if (!tfile) return -1;
result = afc_truncate_file(afc, tfile, size);
afc_close_file(afc, tfile);
return result;
}
int ifuse_ftruncate(const char *path, off_t size, struct fuse_file_info *fi) {
int result = 0;
AFClient *afc = fuse_get_context()->private_data;
AFCFile *file = g_hash_table_lookup(file_handles, &fi->fh);
if (!file) return -ENOENT;
return afc_truncate_file(afc, file, size);
}
int ifuse_unlink(const char *path) {
AFClient *afc = fuse_get_context()->private_data;
if (afc_delete_file(afc, path)) return 0;
else return -1;
}
int ifuse_rename(const char *from, const char *to) {
AFClient *afc = fuse_get_context()->private_data;
if (afc_rename_file(afc, from, to)) return 0;
else return -1;
}
int ifuse_mkdir(const char *dir, mode_t ignored) {
AFClient *afc = fuse_get_context()->private_data;
if (afc_mkdir(afc, dir)) return 0;
else return -1;
}
static struct fuse_operations ifuse_oper = {
.getattr = ifuse_getattr,
.statfs = ifuse_statfs,
.readdir = ifuse_readdir,
.mkdir = ifuse_mkdir,
.rmdir = ifuse_unlink, // AFC uses the same op for both.
.create = ifuse_create,
.open = ifuse_open,
.read = ifuse_read,
.write = ifuse_write,
.truncate = ifuse_truncate,
.ftruncate = ifuse_ftruncate,
.unlink = ifuse_unlink,
.rename = ifuse_rename,
.fsync = ifuse_fsync,
.release = ifuse_release,
.init = ifuse_init,
.destroy = ifuse_cleanup
......
......@@ -80,6 +80,13 @@ int main(int argc, char *argv[]) {
}
free_dictionary(dirs);
dirs = afc_get_devinfo(afc);
if (dirs) {
for (i = 0; strcmp(dirs[i], ""); i+=2) {
printf("%s: %s\n", dirs[i], dirs[i+1]);
}
}
AFCFile *my_file = afc_open_file(afc, "/iTunesOnTheGoPlaylist.plist", AFC_FILE_READ);
if (my_file) {
printf("A file size: %i\n", my_file->size);
......@@ -108,12 +115,25 @@ int main(int argc, char *argv[]) {
printf("Deleting a file...\n");
bytes = afc_delete_file(afc, "/delme");
if (bytes) printf("Success.\n");
else printf("Failure.\n");
else printf("Failure. (expected unless you have a /delme file on your phone)\n");
printf("Renaming a file...\n");
bytes = afc_rename_file(afc, "/renme", "/renme2");
if (bytes > 0) printf("Success.\n");
else printf("Failure.\n");
else printf("Failure. (expected unless you have a /renme file on your phone)\n");
printf("Seek & read\n");
my_file = afc_open_file(afc, "/readme.libiphone.fx", AFC_FILE_READ);
bytes = afc_seek_file(afc, my_file, 5);
if (!bytes) printf("WARN: SEEK DID NOT WORK\n");
char *threeletterword = (char*)malloc(sizeof(char) * 5);
bytes = afc_read_file(afc, my_file, threeletterword, 3);
threeletterword[3] = '\0';
if (bytes > 0) printf("Result: %s\n", threeletterword);
else printf("Couldn't read!\n");
free(threeletterword);
afc_close_file(afc, my_file);
}
afc_disconnect(afc);
} else {
......
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