2012-01-26 21:00:45 +00:00
|
|
|
#include <unistd.h>
|
|
|
|
#include <fcntl.h>
|
|
|
|
#include <sys/stat.h>
|
|
|
|
#include <stdlib.h>
|
2012-01-27 01:43:33 +00:00
|
|
|
#include <string.h>
|
2012-01-26 21:00:45 +00:00
|
|
|
#include <errno.h>
|
2013-10-06 15:55:58 +00:00
|
|
|
#include "gettext.h"
|
2013-05-11 19:33:46 +00:00
|
|
|
#include <zip.h>
|
2013-10-19 05:17:13 +00:00
|
|
|
#include <time.h>
|
2012-01-26 21:00:45 +00:00
|
|
|
|
|
|
|
#include "dive.h"
|
2012-01-27 20:43:40 +00:00
|
|
|
#include "file.h"
|
2012-01-26 21:00:45 +00:00
|
|
|
|
2012-08-24 22:39:00 +00:00
|
|
|
/* Crazy windows sh*t */
|
|
|
|
#ifndef O_BINARY
|
|
|
|
#define O_BINARY 0
|
|
|
|
#endif
|
|
|
|
|
2013-03-14 03:37:38 +00:00
|
|
|
int readfile(const char *filename, struct memblock *mem)
|
2012-01-26 21:00:45 +00:00
|
|
|
{
|
2012-07-12 22:28:47 +00:00
|
|
|
int ret, fd;
|
2012-01-26 21:00:45 +00:00
|
|
|
struct stat st;
|
2012-01-27 18:56:36 +00:00
|
|
|
char *buf;
|
2012-01-26 21:00:45 +00:00
|
|
|
|
|
|
|
mem->buffer = NULL;
|
|
|
|
mem->size = 0;
|
|
|
|
|
2013-12-19 13:00:51 +00:00
|
|
|
fd = subsurface_open(filename, O_RDONLY | O_BINARY, 0);
|
2012-01-26 21:00:45 +00:00
|
|
|
if (fd < 0)
|
|
|
|
return fd;
|
|
|
|
ret = fstat(fd, &st);
|
|
|
|
if (ret < 0)
|
|
|
|
goto out;
|
|
|
|
ret = -EINVAL;
|
|
|
|
if (!S_ISREG(st.st_mode))
|
|
|
|
goto out;
|
|
|
|
ret = 0;
|
|
|
|
if (!st.st_size)
|
|
|
|
goto out;
|
2014-02-28 04:09:57 +00:00
|
|
|
buf = malloc(st.st_size + 1);
|
2012-01-26 21:00:45 +00:00
|
|
|
ret = -1;
|
|
|
|
errno = ENOMEM;
|
2012-01-27 18:56:36 +00:00
|
|
|
if (!buf)
|
2012-01-26 21:00:45 +00:00
|
|
|
goto out;
|
2012-01-27 18:56:36 +00:00
|
|
|
mem->buffer = buf;
|
2012-01-26 21:00:45 +00:00
|
|
|
mem->size = st.st_size;
|
2012-01-27 18:56:36 +00:00
|
|
|
ret = read(fd, buf, mem->size);
|
2012-01-26 21:00:45 +00:00
|
|
|
if (ret < 0)
|
|
|
|
goto free;
|
2012-01-27 18:56:36 +00:00
|
|
|
buf[ret] = 0;
|
2012-01-26 21:00:45 +00:00
|
|
|
if (ret == mem->size)
|
|
|
|
goto out;
|
|
|
|
errno = EIO;
|
|
|
|
ret = -1;
|
|
|
|
free:
|
|
|
|
free(mem->buffer);
|
|
|
|
mem->buffer = NULL;
|
|
|
|
mem->size = 0;
|
|
|
|
out:
|
|
|
|
close(fd);
|
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
|
2012-01-27 01:43:33 +00:00
|
|
|
|
2014-03-14 18:26:07 +00:00
|
|
|
static void zip_read(struct zip_file *file, const char *filename)
|
2012-01-27 01:43:33 +00:00
|
|
|
{
|
|
|
|
int size = 1024, n, read = 0;
|
|
|
|
char *mem = malloc(size);
|
|
|
|
|
2014-02-28 04:09:57 +00:00
|
|
|
while ((n = zip_fread(file, mem + read, size - read)) > 0) {
|
2012-01-27 01:43:33 +00:00
|
|
|
read += n;
|
|
|
|
size = read * 3 / 2;
|
|
|
|
mem = realloc(mem, size);
|
|
|
|
}
|
2013-03-17 05:12:23 +00:00
|
|
|
mem[read] = 0;
|
2014-03-14 18:26:07 +00:00
|
|
|
parse_xml_buffer(filename, mem, read, &dive_table, NULL);
|
2012-01-27 01:43:33 +00:00
|
|
|
free(mem);
|
|
|
|
}
|
|
|
|
|
2014-03-14 18:26:07 +00:00
|
|
|
static int try_to_open_zip(const char *filename, struct memblock *mem)
|
2012-01-27 01:43:33 +00:00
|
|
|
{
|
|
|
|
int success = 0;
|
2012-01-27 18:56:36 +00:00
|
|
|
/* Grr. libzip needs to re-open the file, it can't take a buffer */
|
2013-12-19 13:00:51 +00:00
|
|
|
struct zip *zip = subsurface_zip_open_readonly(filename, ZIP_CHECKCONS, NULL);
|
2012-01-27 01:43:33 +00:00
|
|
|
|
|
|
|
if (zip) {
|
|
|
|
int index;
|
2014-02-28 04:09:57 +00:00
|
|
|
for (index = 0;; index++) {
|
2012-01-27 01:43:33 +00:00
|
|
|
struct zip_file *file = zip_fopen_index(zip, index, 0);
|
|
|
|
if (!file)
|
|
|
|
break;
|
2014-10-13 18:31:01 +00:00
|
|
|
/* skip parsing the divelogs.de pictures */
|
|
|
|
if (strstr(zip_get_name(zip, index, 0), "pictures/"))
|
|
|
|
continue;
|
2014-03-14 18:26:07 +00:00
|
|
|
zip_read(file, filename);
|
2012-01-27 01:43:33 +00:00
|
|
|
zip_fclose(file);
|
|
|
|
success++;
|
|
|
|
}
|
2013-12-19 13:00:51 +00:00
|
|
|
subsurface_zip_close(zip);
|
2012-01-27 01:43:33 +00:00
|
|
|
}
|
|
|
|
return success;
|
|
|
|
}
|
|
|
|
|
2014-03-14 18:26:07 +00:00
|
|
|
static int try_to_xslt_open_csv(const char *filename, struct memblock *mem, const char *tag)
|
2013-09-29 12:44:38 +00:00
|
|
|
{
|
|
|
|
char *buf;
|
|
|
|
|
2014-03-14 18:26:07 +00:00
|
|
|
if (readfile(filename, mem) < 0)
|
|
|
|
return report_error(translate("gettextFromC", "Failed to read '%s'"), filename);
|
2013-09-29 12:44:38 +00:00
|
|
|
|
|
|
|
/* Surround the CSV file content with XML tags to enable XSLT
|
|
|
|
* parsing
|
2014-01-16 20:50:14 +00:00
|
|
|
*
|
|
|
|
* Tag markers take: strlen("<></>") = 5
|
2013-09-29 12:44:38 +00:00
|
|
|
*/
|
2014-01-16 20:50:14 +00:00
|
|
|
buf = realloc(mem->buffer, mem->size + 5 + strlen(tag) * 2);
|
2013-09-29 12:44:38 +00:00
|
|
|
if (buf != NULL) {
|
2014-01-16 20:50:14 +00:00
|
|
|
char *starttag = NULL;
|
|
|
|
char *endtag = NULL;
|
|
|
|
|
|
|
|
starttag = malloc(3 + strlen(tag));
|
|
|
|
endtag = malloc(4 + strlen(tag));
|
|
|
|
|
|
|
|
if (starttag == NULL || endtag == NULL) {
|
2014-03-06 22:19:42 +00:00
|
|
|
/* this is fairly silly - so the malloc fails, but we strdup the error?
|
|
|
|
* let's complete the silliness by freeing the two pointers in case one malloc succeeded
|
|
|
|
* and the other one failed - this will make static analysis tools happy */
|
|
|
|
free(starttag);
|
|
|
|
free(endtag);
|
|
|
|
free(buf);
|
2014-03-14 18:26:07 +00:00
|
|
|
return report_error("Memory allocation failed in %s", __func__);
|
2014-01-16 20:50:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
sprintf(starttag, "<%s>", tag);
|
|
|
|
sprintf(endtag, "</%s>", tag);
|
|
|
|
|
|
|
|
memmove(buf + 2 + strlen(tag), buf, mem->size);
|
|
|
|
memcpy(buf, starttag, 2 + strlen(tag));
|
|
|
|
memcpy(buf + mem->size + 2 + strlen(tag), endtag, 4 + strlen(tag));
|
|
|
|
mem->size += (5 + 2 * strlen(tag));
|
2013-12-11 20:21:49 +00:00
|
|
|
mem->buffer = buf;
|
2014-01-16 20:50:14 +00:00
|
|
|
|
|
|
|
free(starttag);
|
|
|
|
free(endtag);
|
2013-12-10 23:53:31 +00:00
|
|
|
} else {
|
|
|
|
free(mem->buffer);
|
2014-03-14 18:26:07 +00:00
|
|
|
return report_error("realloc failed in %s", __func__);
|
2013-12-10 23:53:31 +00:00
|
|
|
}
|
2013-09-29 12:44:38 +00:00
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2014-02-15 06:36:50 +00:00
|
|
|
int db_test_func(void *param, int columns, char **data, char **column)
|
|
|
|
{
|
|
|
|
return *data[0] == '0';
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2014-03-14 18:26:07 +00:00
|
|
|
static int try_to_open_db(const char *filename, struct memblock *mem)
|
2013-03-05 05:10:39 +00:00
|
|
|
{
|
2014-02-15 06:36:49 +00:00
|
|
|
sqlite3 *handle;
|
2014-02-15 06:36:50 +00:00
|
|
|
char dm4_test[] = "select count(*) from sqlite_master where type='table' and name='Dive' and sql like '%ProfileBlob%'";
|
|
|
|
char shearwater_test[] = "select count(*) from sqlite_master where type='table' and name='system' and sql like '%dbVersion%'";
|
2014-02-15 06:36:49 +00:00
|
|
|
int retval;
|
|
|
|
|
|
|
|
retval = sqlite3_open(filename, &handle);
|
|
|
|
|
|
|
|
if (retval) {
|
2014-02-28 04:09:57 +00:00
|
|
|
fprintf(stderr, translate("gettextFromC", "Database connection failed '%s'.\n"), filename);
|
2014-02-15 06:36:49 +00:00
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
2014-02-15 06:36:50 +00:00
|
|
|
/* Testing if DB schema resembles Suunto DM4 database format */
|
|
|
|
retval = sqlite3_exec(handle, dm4_test, &db_test_func, 0, NULL);
|
|
|
|
if (!retval) {
|
2014-03-14 18:26:07 +00:00
|
|
|
retval = parse_dm4_buffer(handle, filename, mem->buffer, mem->size, &dive_table);
|
2014-02-15 06:36:50 +00:00
|
|
|
sqlite3_close(handle);
|
|
|
|
return retval;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Testing if DB schema resembles Shearwater database format */
|
|
|
|
retval = sqlite3_exec(handle, shearwater_test, &db_test_func, 0, NULL);
|
|
|
|
if (!retval) {
|
2014-03-14 18:26:07 +00:00
|
|
|
retval = parse_shearwater_buffer(handle, filename, mem->buffer, mem->size, &dive_table);
|
2014-02-15 06:36:50 +00:00
|
|
|
sqlite3_close(handle);
|
|
|
|
return retval;
|
|
|
|
}
|
|
|
|
|
2014-02-15 06:36:49 +00:00
|
|
|
sqlite3_close(handle);
|
|
|
|
|
|
|
|
return retval;
|
2013-03-05 05:10:39 +00:00
|
|
|
}
|
|
|
|
|
2014-01-27 13:44:26 +00:00
|
|
|
timestamp_t parse_date(const char *date)
|
2012-06-20 03:07:42 +00:00
|
|
|
{
|
|
|
|
int hour, min, sec;
|
|
|
|
struct tm tm;
|
|
|
|
char *p;
|
|
|
|
|
|
|
|
memset(&tm, 0, sizeof(tm));
|
|
|
|
tm.tm_mday = strtol(date, &p, 10);
|
|
|
|
if (tm.tm_mday < 1 || tm.tm_mday > 31)
|
|
|
|
return 0;
|
|
|
|
for (tm.tm_mon = 0; tm.tm_mon < 12; tm.tm_mon++) {
|
|
|
|
if (!memcmp(p, monthname(tm.tm_mon), 3))
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
if (tm.tm_mon > 11)
|
|
|
|
return 0;
|
2014-02-28 04:09:57 +00:00
|
|
|
date = p + 3;
|
2012-06-20 03:07:42 +00:00
|
|
|
tm.tm_year = strtol(date, &p, 10);
|
|
|
|
if (date == p)
|
|
|
|
return 0;
|
|
|
|
if (tm.tm_year < 70)
|
|
|
|
tm.tm_year += 2000;
|
|
|
|
if (tm.tm_year < 100)
|
|
|
|
tm.tm_year += 1900;
|
|
|
|
if (sscanf(p, "%d:%d:%d", &hour, &min, &sec) != 3)
|
|
|
|
return 0;
|
|
|
|
tm.tm_hour = hour;
|
|
|
|
tm.tm_min = min;
|
|
|
|
tm.tm_sec = sec;
|
|
|
|
return utc_mktime(&tm);
|
|
|
|
}
|
|
|
|
|
|
|
|
enum csv_format {
|
2014-02-28 04:09:57 +00:00
|
|
|
CSV_DEPTH,
|
|
|
|
CSV_TEMP,
|
2014-05-28 06:55:46 +00:00
|
|
|
CSV_PRESSURE,
|
|
|
|
POSEIDON_DEPTH,
|
|
|
|
POSEIDON_TEMP,
|
|
|
|
POSEIDON_SETPOINT,
|
|
|
|
POSEIDON_SENSOR1,
|
|
|
|
POSEIDON_SENSOR2,
|
|
|
|
POSEIDON_PRESSURE,
|
|
|
|
POSEIDON_DILUENT
|
2012-06-20 03:07:42 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
static void add_sample_data(struct sample *sample, enum csv_format type, double val)
|
|
|
|
{
|
|
|
|
switch (type) {
|
|
|
|
case CSV_DEPTH:
|
|
|
|
sample->depth.mm = feet_to_mm(val);
|
|
|
|
break;
|
|
|
|
case CSV_TEMP:
|
|
|
|
sample->temperature.mkelvin = F_to_mkelvin(val);
|
|
|
|
break;
|
|
|
|
case CSV_PRESSURE:
|
2014-02-28 04:09:57 +00:00
|
|
|
sample->cylinderpressure.mbar = psi_to_mbar(val * 4);
|
2012-06-20 03:07:42 +00:00
|
|
|
break;
|
2014-05-28 06:55:46 +00:00
|
|
|
case POSEIDON_DEPTH:
|
|
|
|
sample->depth.mm = val * 0.5 *1000;
|
|
|
|
break;
|
|
|
|
case POSEIDON_TEMP:
|
|
|
|
sample->temperature.mkelvin = C_to_mkelvin(val * 0.2);
|
|
|
|
break;
|
|
|
|
case POSEIDON_SETPOINT:
|
|
|
|
sample->setpoint.mbar = val * 10;
|
|
|
|
break;
|
|
|
|
case POSEIDON_SENSOR1:
|
|
|
|
sample->o2sensor[0].mbar = val * 10;
|
|
|
|
break;
|
|
|
|
case POSEIDON_SENSOR2:
|
|
|
|
sample->o2sensor[1].mbar = val * 10;
|
|
|
|
break;
|
|
|
|
case POSEIDON_PRESSURE:
|
|
|
|
sample->cylinderpressure.mbar = val * 1000;
|
|
|
|
break;
|
|
|
|
case POSEIDON_DILUENT:
|
|
|
|
sample->diluentpressure.mbar = val * 1000;
|
|
|
|
break;
|
2012-06-20 03:07:42 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Cochran comma-separated values: depth in feet, temperature in F, pressure in psi.
|
|
|
|
*
|
|
|
|
* They start with eight comma-separated fields like:
|
|
|
|
*
|
|
|
|
* filename: {C:\Analyst4\can\T036785.can},{C:\Analyst4\can\K031892.can}
|
|
|
|
* divenr: %d
|
|
|
|
* datetime: {03Sep11 16:37:22},{15Dec11 18:27:02}
|
|
|
|
* ??: 1
|
|
|
|
* serialnr??: {CCI134},{CCI207}
|
|
|
|
* computer??: {GeminiII},{CommanderIII}
|
|
|
|
* computer??: {GeminiII},{CommanderIII}
|
|
|
|
* ??: 1
|
|
|
|
*
|
|
|
|
* Followed by the data values (all comma-separated, all one long line).
|
|
|
|
*/
|
|
|
|
static int try_to_open_csv(const char *filename, struct memblock *mem, enum csv_format type)
|
|
|
|
{
|
|
|
|
char *p = mem->buffer;
|
|
|
|
char *header[8];
|
|
|
|
int i, time;
|
2012-09-20 00:35:52 +00:00
|
|
|
timestamp_t date;
|
2012-06-20 03:07:42 +00:00
|
|
|
struct dive *dive;
|
2013-02-09 15:41:15 +00:00
|
|
|
struct divecomputer *dc;
|
2012-06-20 03:07:42 +00:00
|
|
|
|
|
|
|
for (i = 0; i < 8; i++) {
|
|
|
|
header[i] = p;
|
|
|
|
p = strchr(p, ',');
|
|
|
|
if (!p)
|
|
|
|
return 0;
|
|
|
|
p++;
|
|
|
|
}
|
|
|
|
|
|
|
|
date = parse_date(header[2]);
|
|
|
|
if (!date)
|
|
|
|
return 0;
|
|
|
|
|
|
|
|
dive = alloc_dive();
|
|
|
|
dive->when = date;
|
|
|
|
dive->number = atoi(header[1]);
|
2013-02-09 15:41:15 +00:00
|
|
|
dc = &dive->dc;
|
2012-06-20 03:07:42 +00:00
|
|
|
|
|
|
|
time = 0;
|
|
|
|
for (;;) {
|
|
|
|
char *end;
|
|
|
|
double val;
|
|
|
|
struct sample *sample;
|
|
|
|
|
|
|
|
errno = 0;
|
2014-02-28 04:09:57 +00:00
|
|
|
val = strtod(p, &end); // FIXME == localization issue
|
2012-06-20 03:07:42 +00:00
|
|
|
if (end == p)
|
|
|
|
break;
|
|
|
|
if (errno)
|
|
|
|
break;
|
|
|
|
|
2013-02-09 15:41:15 +00:00
|
|
|
sample = prepare_sample(dc);
|
2012-06-20 03:07:42 +00:00
|
|
|
sample->time.seconds = time;
|
|
|
|
add_sample_data(sample, type, val);
|
2013-02-09 15:41:15 +00:00
|
|
|
finish_sample(dc);
|
2012-06-20 03:07:42 +00:00
|
|
|
|
|
|
|
time++;
|
2013-02-09 15:41:15 +00:00
|
|
|
dc->duration.seconds = time;
|
2012-06-20 03:07:42 +00:00
|
|
|
if (*end != ',')
|
|
|
|
break;
|
2014-02-28 04:09:57 +00:00
|
|
|
p = end + 1;
|
2012-06-20 03:07:42 +00:00
|
|
|
}
|
|
|
|
record_dive(dive);
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
2014-03-14 18:26:07 +00:00
|
|
|
static int open_by_filename(const char *filename, const char *fmt, struct memblock *mem)
|
2012-01-27 01:43:33 +00:00
|
|
|
{
|
2013-09-17 18:23:54 +00:00
|
|
|
/* Suunto Dive Manager files: SDE, ZIP; divelogs.de files: DLD */
|
|
|
|
if (!strcasecmp(fmt, "SDE") || !strcasecmp(fmt, "ZIP") || !strcasecmp(fmt, "DLD"))
|
2014-03-14 18:26:07 +00:00
|
|
|
return try_to_open_zip(filename, mem);
|
2012-01-27 01:43:33 +00:00
|
|
|
|
2013-09-29 12:44:38 +00:00
|
|
|
/* CSV files */
|
2014-01-15 13:59:25 +00:00
|
|
|
if (!strcasecmp(fmt, "CSV"))
|
2013-10-17 19:05:29 +00:00
|
|
|
return 1;
|
2013-09-29 12:44:38 +00:00
|
|
|
|
2013-05-12 03:07:28 +00:00
|
|
|
#if ONCE_COCHRAN_IS_SUPPORTED
|
2012-01-27 20:43:40 +00:00
|
|
|
/* Truly nasty intentionally obfuscated Cochran Anal software */
|
|
|
|
if (!strcasecmp(fmt, "CAN"))
|
2014-03-14 18:26:07 +00:00
|
|
|
return try_to_open_cochran(filename, mem);
|
2013-05-12 03:07:28 +00:00
|
|
|
#endif
|
2012-01-27 20:43:40 +00:00
|
|
|
|
2012-06-20 03:07:42 +00:00
|
|
|
/* Cochran export comma-separated-value files */
|
|
|
|
if (!strcasecmp(fmt, "DPT"))
|
|
|
|
return try_to_open_csv(filename, mem, CSV_DEPTH);
|
|
|
|
if (!strcasecmp(fmt, "TMP"))
|
|
|
|
return try_to_open_csv(filename, mem, CSV_TEMP);
|
|
|
|
if (!strcasecmp(fmt, "HP1"))
|
|
|
|
return try_to_open_csv(filename, mem, CSV_PRESSURE);
|
|
|
|
|
2012-01-27 01:43:33 +00:00
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2014-03-14 18:26:07 +00:00
|
|
|
static void parse_file_buffer(const char *filename, struct memblock *mem)
|
2012-01-27 18:56:36 +00:00
|
|
|
{
|
|
|
|
char *fmt = strrchr(filename, '.');
|
2014-03-14 18:26:07 +00:00
|
|
|
if (fmt && open_by_filename(filename, fmt + 1, mem))
|
2012-01-27 18:56:36 +00:00
|
|
|
return;
|
|
|
|
|
2013-12-09 06:42:51 +00:00
|
|
|
if (!mem->size || !mem->buffer)
|
|
|
|
return;
|
|
|
|
|
2014-03-14 18:26:07 +00:00
|
|
|
parse_xml_buffer(filename, mem->buffer, mem->size, &dive_table, NULL);
|
2012-01-27 18:56:36 +00:00
|
|
|
}
|
|
|
|
|
2014-03-14 18:26:07 +00:00
|
|
|
int parse_file(const char *filename)
|
2012-01-26 21:00:45 +00:00
|
|
|
{
|
2014-03-12 21:12:58 +00:00
|
|
|
struct git_repository *git;
|
|
|
|
const char *branch;
|
2012-01-26 21:00:45 +00:00
|
|
|
struct memblock mem;
|
2013-03-05 05:10:39 +00:00
|
|
|
char *fmt;
|
2012-01-26 21:00:45 +00:00
|
|
|
|
2014-03-12 21:12:58 +00:00
|
|
|
git = is_git_repository(filename, &branch);
|
|
|
|
if (git && !git_load_dives(git, branch))
|
2014-03-14 18:26:07 +00:00
|
|
|
return 0;
|
2014-03-12 21:12:58 +00:00
|
|
|
|
2012-01-26 21:00:45 +00:00
|
|
|
if (readfile(filename, &mem) < 0) {
|
2012-09-15 11:44:00 +00:00
|
|
|
/* we don't want to display an error if this was the default file */
|
2014-02-28 04:09:57 +00:00
|
|
|
if (prefs.default_filename && !strcmp(filename, prefs.default_filename))
|
2014-03-14 18:26:07 +00:00
|
|
|
return 0;
|
2012-11-10 14:32:06 +00:00
|
|
|
|
2014-03-14 18:26:07 +00:00
|
|
|
return report_error(translate("gettextFromC", "Failed to read '%s'"), filename);
|
2012-01-26 21:00:45 +00:00
|
|
|
}
|
|
|
|
|
2013-03-05 05:10:39 +00:00
|
|
|
fmt = strrchr(filename, '.');
|
2013-03-07 04:18:42 +00:00
|
|
|
if (fmt && (!strcasecmp(fmt + 1, "DB") || !strcasecmp(fmt + 1, "BAK"))) {
|
2014-03-14 18:26:07 +00:00
|
|
|
if (!try_to_open_db(filename, &mem)) {
|
2013-03-05 05:10:39 +00:00
|
|
|
free(mem.buffer);
|
2014-03-14 18:26:07 +00:00
|
|
|
return 0;
|
2013-03-05 05:10:39 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-03-14 18:26:07 +00:00
|
|
|
parse_file_buffer(filename, &mem);
|
2012-01-26 21:00:45 +00:00
|
|
|
free(mem.buffer);
|
2014-03-14 18:26:07 +00:00
|
|
|
return 0;
|
2012-01-26 21:00:45 +00:00
|
|
|
}
|
2013-10-16 19:05:19 +00:00
|
|
|
|
2014-05-28 06:55:46 +00:00
|
|
|
#define MATCH(buffer, pattern) \
|
|
|
|
memcmp(buffer, pattern, strlen(pattern))
|
|
|
|
|
|
|
|
char *parse_mkvi_value(const char *haystack, const char *needle)
|
|
|
|
{
|
|
|
|
char *lineptr, *valueptr, *endptr, *ret = NULL;
|
|
|
|
|
|
|
|
if ((lineptr = strstr(haystack, needle)) != NULL) {
|
|
|
|
if ((valueptr = strstr(lineptr, ": ")) != NULL) {
|
|
|
|
valueptr += 2;
|
|
|
|
}
|
|
|
|
if ((endptr = strstr(lineptr, "\n")) != NULL) {
|
|
|
|
*endptr = 0;
|
|
|
|
ret = strdup(valueptr);
|
|
|
|
*endptr = '\n';
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
|
|
|
|
static int cur_cylinder_index;
|
|
|
|
int parse_txt_file(const char *filename, const char *csv)
|
|
|
|
{
|
|
|
|
struct memblock memtxt, memcsv;
|
|
|
|
|
|
|
|
if (readfile(filename, &memtxt) < 0) {
|
|
|
|
return report_error(translate("gettextFromC", "Failed to read '%s'"), filename);
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* MkVI stores some information in .txt file but the whole profile and events are stored in .csv file. First
|
|
|
|
* make sure the input .txt looks like proper MkVI file, then start parsing the .csv.
|
|
|
|
*/
|
|
|
|
if (MATCH(memtxt.buffer, "MkVI_Config") == 0) {
|
|
|
|
int d, m, y;
|
|
|
|
int hh = 0, mm = 0, ss = 0;
|
2014-10-27 16:48:38 +00:00
|
|
|
int prev_depth = 0, cur_sampletime = 0, prev_setpoint = -1;
|
2014-10-27 16:19:54 +00:00
|
|
|
bool has_depth = false, has_setpoint = false;
|
2014-05-28 06:55:46 +00:00
|
|
|
char *lineptr;
|
|
|
|
|
|
|
|
struct dive *dive;
|
|
|
|
struct divecomputer *dc;
|
|
|
|
struct tm cur_tm;
|
|
|
|
|
|
|
|
if (sscanf(parse_mkvi_value(memtxt.buffer, "Dive started at"), "%d-%d-%d %d:%d:%d",
|
|
|
|
&y, &m, &d, &hh, &mm, &ss) != 6) {
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
cur_tm.tm_year = y;
|
|
|
|
cur_tm.tm_mon = m - 1;
|
|
|
|
cur_tm.tm_mday = d;
|
|
|
|
cur_tm.tm_hour = hh;
|
|
|
|
cur_tm.tm_min = mm;
|
|
|
|
cur_tm.tm_sec = ss;
|
|
|
|
|
|
|
|
dive = alloc_dive();
|
|
|
|
dive->when = utc_mktime(&cur_tm);;
|
|
|
|
dive->dc.model = strdup("Poseidon MkVI Discovery");
|
|
|
|
dive->dc.deviceid = atoi(parse_mkvi_value(memtxt.buffer, "Rig Serial number"));
|
|
|
|
dive->dc.dctype = CCR;
|
|
|
|
|
|
|
|
dive->cylinder[cur_cylinder_index].type.size.mliter = 3000;
|
|
|
|
dive->cylinder[cur_cylinder_index].type.workingpressure.mbar = 200000;
|
|
|
|
dive->cylinder[cur_cylinder_index].type.description = strdup("3l Mk6");
|
|
|
|
cur_cylinder_index++;
|
|
|
|
|
|
|
|
dive->cylinder[cur_cylinder_index].type.size.mliter = 3000;
|
|
|
|
dive->cylinder[cur_cylinder_index].type.workingpressure.mbar = 200000;
|
|
|
|
dive->cylinder[cur_cylinder_index].type.description = strdup("3l Mk6");
|
|
|
|
cur_cylinder_index++;
|
|
|
|
|
|
|
|
dc = &dive->dc;
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Read samples from the CSV file. A sample contains all the lines with same timestamp. The CSV file has
|
|
|
|
* the following format:
|
|
|
|
*
|
|
|
|
* timestamp, type, value
|
|
|
|
*
|
|
|
|
* And following fields are of interest to us:
|
|
|
|
*
|
|
|
|
* 6 sensor1
|
|
|
|
* 7 sensor2
|
|
|
|
* 8 depth
|
|
|
|
* 13 o2 tank pressure
|
|
|
|
* 14 diluent tank pressure
|
|
|
|
* 20 o2 setpoint
|
|
|
|
* 39 water temp
|
|
|
|
*/
|
|
|
|
|
|
|
|
if (readfile(csv, &memcsv) < 0) {
|
|
|
|
return report_error(translate("gettextFromC", "Poseidon import failed: unable to read '%s'"), csv);
|
|
|
|
}
|
|
|
|
lineptr = memcsv.buffer;
|
|
|
|
for (;;) {
|
|
|
|
struct sample *sample;
|
|
|
|
int type;
|
|
|
|
int value;
|
|
|
|
int sampletime;
|
|
|
|
|
|
|
|
/* Collect all the information for one sample */
|
|
|
|
sscanf(lineptr, "%d,%d,%d", &cur_sampletime, &type, &value);
|
|
|
|
|
|
|
|
has_depth = false;
|
2014-10-27 16:19:54 +00:00
|
|
|
has_setpoint = false;
|
2014-05-28 06:55:46 +00:00
|
|
|
sample = prepare_sample(dc);
|
|
|
|
sample->time.seconds = cur_sampletime;
|
|
|
|
|
|
|
|
do {
|
|
|
|
int i = sscanf(lineptr, "%d,%d,%d", &sampletime, &type, &value);
|
|
|
|
switch (i) {
|
|
|
|
case 3:
|
|
|
|
switch (type) {
|
|
|
|
case 6:
|
|
|
|
add_sample_data(sample, POSEIDON_SENSOR1, value);
|
|
|
|
break;
|
|
|
|
case 7:
|
|
|
|
add_sample_data(sample, POSEIDON_SENSOR2, value);
|
|
|
|
break;
|
|
|
|
case 8:
|
|
|
|
has_depth = true;
|
|
|
|
prev_depth = value;
|
|
|
|
add_sample_data(sample, POSEIDON_DEPTH, value);
|
|
|
|
break;
|
|
|
|
case 13:
|
|
|
|
add_sample_data(sample, POSEIDON_PRESSURE, value);
|
|
|
|
break;
|
|
|
|
case 14:
|
|
|
|
add_sample_data(sample, POSEIDON_DILUENT, value);
|
|
|
|
break;
|
|
|
|
case 20:
|
2014-10-27 16:19:54 +00:00
|
|
|
has_setpoint = true;
|
|
|
|
prev_setpoint = value;
|
2014-05-28 06:55:46 +00:00
|
|
|
add_sample_data(sample, POSEIDON_SETPOINT, value);
|
|
|
|
break;
|
|
|
|
case 39:
|
|
|
|
add_sample_data(sample, POSEIDON_TEMP, value);
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
break;
|
|
|
|
} /* sample types */
|
|
|
|
break;
|
|
|
|
case EOF:
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
printf("Unable to parse input: %s\n", lineptr);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
lineptr = strchr(lineptr, '\n');
|
|
|
|
if (!lineptr || !*lineptr)
|
|
|
|
break;
|
|
|
|
lineptr++;
|
|
|
|
|
|
|
|
/* Grabbing next sample time */
|
|
|
|
sscanf(lineptr, "%d,%d,%d", &cur_sampletime, &type, &value);
|
|
|
|
} while (sampletime == cur_sampletime);
|
|
|
|
|
|
|
|
if (!has_depth)
|
|
|
|
add_sample_data(sample, POSEIDON_DEPTH, prev_depth);
|
2014-10-27 16:19:54 +00:00
|
|
|
if (!has_setpoint)
|
|
|
|
add_sample_data(sample, POSEIDON_SETPOINT, prev_setpoint);
|
2014-05-28 06:55:46 +00:00
|
|
|
finish_sample(dc);
|
|
|
|
|
|
|
|
if (!lineptr || !*lineptr)
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
record_dive(dive);
|
|
|
|
return 1;
|
|
|
|
} else {
|
|
|
|
return report_error(translate("gettextFromC", "No matching DC found for file '%s'"), csv);
|
|
|
|
}
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2013-10-16 19:05:19 +00:00
|
|
|
#define MAXCOLDIGITS 3
|
|
|
|
#define MAXCOLS 100
|
2014-07-10 18:54:18 +00:00
|
|
|
int parse_csv_file(const char *filename, int timef, int depthf, int tempf, int po2f, int cnsf, int ndlf, int ttsf, int stopdepthf, int pressuref, int sepidx, const char *csvtemplate, int unitidx)
|
2013-10-16 19:05:19 +00:00
|
|
|
{
|
|
|
|
struct memblock mem;
|
2014-02-28 04:09:57 +00:00
|
|
|
int pnr = 0;
|
2014-07-10 18:54:18 +00:00
|
|
|
char *params[27];
|
2013-10-16 19:05:19 +00:00
|
|
|
char timebuf[MAXCOLDIGITS];
|
|
|
|
char depthbuf[MAXCOLDIGITS];
|
|
|
|
char tempbuf[MAXCOLDIGITS];
|
2013-11-21 22:48:40 +00:00
|
|
|
char po2buf[MAXCOLDIGITS];
|
2013-11-21 22:48:41 +00:00
|
|
|
char cnsbuf[MAXCOLDIGITS];
|
2014-07-09 20:13:37 +00:00
|
|
|
char ndlbuf[MAXCOLDIGITS];
|
2014-07-09 20:13:38 +00:00
|
|
|
char ttsbuf[MAXCOLDIGITS];
|
2013-11-21 22:48:42 +00:00
|
|
|
char stopdepthbuf[MAXCOLDIGITS];
|
2014-07-10 18:54:18 +00:00
|
|
|
char pressurebuf[MAXCOLDIGITS];
|
2014-02-15 08:51:23 +00:00
|
|
|
char unitbuf[MAXCOLDIGITS];
|
2013-12-04 23:19:28 +00:00
|
|
|
char separator_index[MAXCOLDIGITS];
|
2013-10-19 05:17:13 +00:00
|
|
|
time_t now;
|
|
|
|
struct tm *timep;
|
|
|
|
char curdate[9];
|
|
|
|
char curtime[6];
|
2013-10-16 19:05:19 +00:00
|
|
|
|
2014-07-10 18:54:18 +00:00
|
|
|
if (timef >= MAXCOLS || depthf >= MAXCOLS || tempf >= MAXCOLS || po2f >= MAXCOLS || cnsf >= MAXCOLS || ndlf >= MAXCOLS || cnsf >= MAXCOLS || stopdepthf >= MAXCOLS || pressuref >= MAXCOLS)
|
2014-03-14 18:26:07 +00:00
|
|
|
return report_error(translate("gettextFromC", "Maximum number of supported columns on CSV import is %d"), MAXCOLS);
|
2013-10-16 19:05:19 +00:00
|
|
|
|
2013-10-19 05:17:13 +00:00
|
|
|
snprintf(timebuf, MAXCOLDIGITS, "%d", timef);
|
|
|
|
snprintf(depthbuf, MAXCOLDIGITS, "%d", depthf);
|
|
|
|
snprintf(tempbuf, MAXCOLDIGITS, "%d", tempf);
|
2013-11-21 22:48:40 +00:00
|
|
|
snprintf(po2buf, MAXCOLDIGITS, "%d", po2f);
|
2013-11-21 22:48:41 +00:00
|
|
|
snprintf(cnsbuf, MAXCOLDIGITS, "%d", cnsf);
|
2014-07-09 20:13:37 +00:00
|
|
|
snprintf(ndlbuf, MAXCOLDIGITS, "%d", ndlf);
|
2014-07-09 20:13:38 +00:00
|
|
|
snprintf(ttsbuf, MAXCOLDIGITS, "%d", ttsf);
|
2013-11-21 22:48:42 +00:00
|
|
|
snprintf(stopdepthbuf, MAXCOLDIGITS, "%d", stopdepthf);
|
2014-07-10 18:54:18 +00:00
|
|
|
snprintf(pressurebuf, MAXCOLDIGITS, "%d", pressuref);
|
2013-12-04 23:19:28 +00:00
|
|
|
snprintf(separator_index, MAXCOLDIGITS, "%d", sepidx);
|
2014-02-15 08:51:23 +00:00
|
|
|
snprintf(unitbuf, MAXCOLDIGITS, "%d", unitidx);
|
2013-10-19 05:17:13 +00:00
|
|
|
time(&now);
|
|
|
|
timep = localtime(&now);
|
|
|
|
strftime(curdate, sizeof(curdate), "%Y%m%d", timep);
|
|
|
|
|
|
|
|
/* As the parameter is numeric, we need to ensure that the leading zero
|
|
|
|
* is not discarded during the transform, thus prepend time with 1 */
|
|
|
|
strftime(curtime, sizeof(curtime), "1%H%M", timep);
|
2013-10-16 19:05:19 +00:00
|
|
|
|
2013-11-21 22:48:39 +00:00
|
|
|
params[pnr++] = "timeField";
|
|
|
|
params[pnr++] = timebuf;
|
|
|
|
params[pnr++] = "depthField";
|
|
|
|
params[pnr++] = depthbuf;
|
|
|
|
params[pnr++] = "tempField";
|
|
|
|
params[pnr++] = tempbuf;
|
2013-11-21 22:48:40 +00:00
|
|
|
params[pnr++] = "po2Field";
|
|
|
|
params[pnr++] = po2buf;
|
2013-11-21 22:48:41 +00:00
|
|
|
params[pnr++] = "cnsField";
|
|
|
|
params[pnr++] = cnsbuf;
|
2014-07-09 20:13:37 +00:00
|
|
|
params[pnr++] = "ndlField";
|
|
|
|
params[pnr++] = ndlbuf;
|
2014-07-09 20:13:38 +00:00
|
|
|
params[pnr++] = "ttsField";
|
|
|
|
params[pnr++] = ttsbuf;
|
2013-11-21 22:48:42 +00:00
|
|
|
params[pnr++] = "stopdepthField";
|
|
|
|
params[pnr++] = stopdepthbuf;
|
2014-07-10 18:54:18 +00:00
|
|
|
params[pnr++] = "pressureField";
|
|
|
|
params[pnr++] = pressurebuf;
|
2013-11-21 22:48:39 +00:00
|
|
|
params[pnr++] = "date";
|
|
|
|
params[pnr++] = curdate;
|
|
|
|
params[pnr++] = "time";
|
|
|
|
params[pnr++] = curtime;
|
2014-02-15 08:51:23 +00:00
|
|
|
params[pnr++] = "units";
|
|
|
|
params[pnr++] = unitbuf;
|
2013-12-04 23:19:28 +00:00
|
|
|
params[pnr++] = "separatorIndex";
|
|
|
|
params[pnr++] = separator_index;
|
2013-11-21 22:48:39 +00:00
|
|
|
params[pnr++] = NULL;
|
2013-10-16 19:05:19 +00:00
|
|
|
|
|
|
|
if (filename == NULL)
|
2014-03-14 18:26:07 +00:00
|
|
|
return report_error("No CSV filename");
|
2013-10-16 19:05:19 +00:00
|
|
|
|
2014-03-14 18:26:07 +00:00
|
|
|
if (try_to_xslt_open_csv(filename, &mem, csvtemplate))
|
|
|
|
return -1;
|
2013-10-16 19:05:19 +00:00
|
|
|
|
2014-03-14 18:26:07 +00:00
|
|
|
parse_xml_buffer(filename, mem.buffer, mem.size, &dive_table, (const char **)params);
|
2013-10-16 19:05:19 +00:00
|
|
|
free(mem.buffer);
|
2014-03-14 18:26:07 +00:00
|
|
|
return 0;
|
2013-10-16 19:05:19 +00:00
|
|
|
}
|
2014-01-25 07:49:23 +00:00
|
|
|
|
2014-03-14 18:26:07 +00:00
|
|
|
int parse_manual_file(const char *filename, int sepidx, int units, int numberf, int datef, int timef, int durationf, int locationf, int gpsf, int maxdepthf, int meandepthf, int buddyf, int notesf, int weightf, int tagsf)
|
2014-01-25 07:49:23 +00:00
|
|
|
{
|
|
|
|
struct memblock mem;
|
2014-02-28 04:09:57 +00:00
|
|
|
int pnr = 0;
|
2014-01-25 07:49:23 +00:00
|
|
|
char *params[33];
|
|
|
|
char numberbuf[MAXCOLDIGITS];
|
|
|
|
char datebuf[MAXCOLDIGITS];
|
|
|
|
char timebuf[MAXCOLDIGITS];
|
|
|
|
char durationbuf[MAXCOLDIGITS];
|
|
|
|
char locationbuf[MAXCOLDIGITS];
|
|
|
|
char gpsbuf[MAXCOLDIGITS];
|
|
|
|
char maxdepthbuf[MAXCOLDIGITS];
|
|
|
|
char meandepthbuf[MAXCOLDIGITS];
|
|
|
|
char buddybuf[MAXCOLDIGITS];
|
|
|
|
char notesbuf[MAXCOLDIGITS];
|
|
|
|
char weightbuf[MAXCOLDIGITS];
|
|
|
|
char tagsbuf[MAXCOLDIGITS];
|
|
|
|
char separator_index[MAXCOLDIGITS];
|
|
|
|
char unit[MAXCOLDIGITS];
|
|
|
|
time_t now;
|
|
|
|
struct tm *timep;
|
|
|
|
char curdate[9];
|
|
|
|
char curtime[6];
|
|
|
|
|
2014-03-14 18:26:07 +00:00
|
|
|
if (numberf >= MAXCOLS || datef >= MAXCOLS || timef >= MAXCOLS || durationf >= MAXCOLS || locationf >= MAXCOLS || gpsf >= MAXCOLS || maxdepthf >= MAXCOLS || meandepthf >= MAXCOLS || buddyf >= MAXCOLS || notesf >= MAXCOLS || weightf >= MAXCOLS || tagsf >= MAXCOLS)
|
|
|
|
return report_error(translate("gettextFromC", "Maximum number of supported columns on CSV import is %d"), MAXCOLS);
|
2014-01-25 07:49:23 +00:00
|
|
|
|
|
|
|
snprintf(numberbuf, MAXCOLDIGITS, "%d", numberf);
|
|
|
|
snprintf(datebuf, MAXCOLDIGITS, "%d", datef);
|
|
|
|
snprintf(timebuf, MAXCOLDIGITS, "%d", timef);
|
|
|
|
snprintf(durationbuf, MAXCOLDIGITS, "%d", durationf);
|
|
|
|
snprintf(locationbuf, MAXCOLDIGITS, "%d", locationf);
|
|
|
|
snprintf(gpsbuf, MAXCOLDIGITS, "%d", gpsf);
|
|
|
|
snprintf(maxdepthbuf, MAXCOLDIGITS, "%d", maxdepthf);
|
|
|
|
snprintf(meandepthbuf, MAXCOLDIGITS, "%d", meandepthf);
|
|
|
|
snprintf(buddybuf, MAXCOLDIGITS, "%d", buddyf);
|
|
|
|
snprintf(notesbuf, MAXCOLDIGITS, "%d", notesf);
|
|
|
|
snprintf(weightbuf, MAXCOLDIGITS, "%d", weightf);
|
|
|
|
snprintf(tagsbuf, MAXCOLDIGITS, "%d", tagsf);
|
|
|
|
snprintf(separator_index, MAXCOLDIGITS, "%d", sepidx);
|
|
|
|
snprintf(unit, MAXCOLDIGITS, "%d", units);
|
|
|
|
time(&now);
|
|
|
|
timep = localtime(&now);
|
|
|
|
strftime(curdate, sizeof(curdate), "%Y%m%d", timep);
|
|
|
|
|
|
|
|
/* As the parameter is numeric, we need to ensure that the leading zero
|
|
|
|
* is not discarded during the transform, thus prepend time with 1 */
|
|
|
|
strftime(curtime, sizeof(curtime), "1%H%M", timep);
|
|
|
|
|
|
|
|
params[pnr++] = "numberField";
|
|
|
|
params[pnr++] = numberbuf;
|
|
|
|
params[pnr++] = "dateField";
|
|
|
|
params[pnr++] = datebuf;
|
|
|
|
params[pnr++] = "timeField";
|
|
|
|
params[pnr++] = timebuf;
|
|
|
|
params[pnr++] = "durationField";
|
|
|
|
params[pnr++] = durationbuf;
|
|
|
|
params[pnr++] = "locationField";
|
|
|
|
params[pnr++] = locationbuf;
|
|
|
|
params[pnr++] = "gpsField";
|
|
|
|
params[pnr++] = gpsbuf;
|
|
|
|
params[pnr++] = "maxDepthField";
|
|
|
|
params[pnr++] = maxdepthbuf;
|
|
|
|
params[pnr++] = "meanDepthField";
|
|
|
|
params[pnr++] = meandepthbuf;
|
|
|
|
params[pnr++] = "buddyField";
|
|
|
|
params[pnr++] = buddybuf;
|
|
|
|
params[pnr++] = "notesField";
|
|
|
|
params[pnr++] = notesbuf;
|
|
|
|
params[pnr++] = "weightField";
|
|
|
|
params[pnr++] = weightbuf;
|
|
|
|
params[pnr++] = "tagsField";
|
|
|
|
params[pnr++] = tagsbuf;
|
|
|
|
params[pnr++] = "date";
|
|
|
|
params[pnr++] = curdate;
|
|
|
|
params[pnr++] = "time";
|
|
|
|
params[pnr++] = curtime;
|
|
|
|
params[pnr++] = "separatorIndex";
|
|
|
|
params[pnr++] = separator_index;
|
|
|
|
params[pnr++] = "units";
|
|
|
|
params[pnr++] = unit;
|
|
|
|
params[pnr++] = NULL;
|
|
|
|
|
|
|
|
if (filename == NULL)
|
2014-03-14 18:26:07 +00:00
|
|
|
return report_error("No manual CSV filename");
|
2014-01-25 07:49:23 +00:00
|
|
|
|
2014-03-14 18:26:07 +00:00
|
|
|
if (try_to_xslt_open_csv(filename, &mem, "manualCSV"))
|
|
|
|
return -1;
|
2014-01-25 07:49:23 +00:00
|
|
|
|
2014-03-14 18:26:07 +00:00
|
|
|
parse_xml_buffer(filename, mem.buffer, mem.size, &dive_table, (const char **)params);
|
2014-01-25 07:49:23 +00:00
|
|
|
free(mem.buffer);
|
2014-03-14 18:26:07 +00:00
|
|
|
return 0;
|
2014-01-25 07:49:23 +00:00
|
|
|
}
|