mirror of
https://github.com/subsurface/subsurface.git
synced 2024-11-30 22:20:21 +00:00
ae81b42fe2
Thise makes initialization of unit types more palatable. For example: surface.time = sample.time - duration_t { .seconds = 20 }; => surface.time = sample.time - 20_sec; delta_depth.mm = feet_to_mm(1.0); // 1ft => delta_depth = 1_ft; get_cylinderid_at_time(..., { .seconds = 20 * 60 + 1 })); => get_cylinderid_at_time(..., 20_min + 1_sec)); Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
1901 lines
52 KiB
C++
1901 lines
52 KiB
C++
// SPDX-License-Identifier: GPL-2.0
|
|
#include <stdio.h>
|
|
#include <ctype.h>
|
|
#include <string.h>
|
|
#include <stdlib.h>
|
|
#include <errno.h>
|
|
#include <sys/types.h>
|
|
#include <sys/stat.h>
|
|
#include <unistd.h>
|
|
#include <fcntl.h>
|
|
#include <git2.h>
|
|
#include <array>
|
|
#include <memory>
|
|
#include <libdivecomputer/parser.h>
|
|
|
|
#include "gettext.h"
|
|
|
|
#include "device.h"
|
|
#include "dive.h"
|
|
#include "divelog.h"
|
|
#include "divesite.h"
|
|
#include "errorhelper.h"
|
|
#include "event.h"
|
|
#include "format.h"
|
|
#include "git-access.h"
|
|
#include "picture.h"
|
|
#include "qthelper.h"
|
|
#include "sample.h"
|
|
#include "subsurface-string.h"
|
|
#include "subsurface-time.h"
|
|
#include "tag.h"
|
|
#include "trip.h"
|
|
#include "version.h"
|
|
|
|
// TODO: Should probably be moved to struct divelog to allow for multi-document
|
|
std::string saved_git_id;
|
|
|
|
struct git_parser_state {
|
|
git_repository *repo = nullptr;
|
|
struct divecomputer *active_dc = nullptr;
|
|
std::unique_ptr<dive> active_dive;
|
|
std::unique_ptr<dive_trip> active_trip;
|
|
std::string fulltext_mode;
|
|
std::string fulltext_query;
|
|
std::string filter_constraint_type;
|
|
std::string filter_constraint_string_mode;
|
|
std::string filter_constraint_range_mode;
|
|
bool filter_constraint_negate = false;
|
|
std::string filter_constraint_data;
|
|
struct picture active_pic;
|
|
struct dive_site *active_site = nullptr;
|
|
std::unique_ptr<filter_preset> active_filter;
|
|
struct divelog *log = nullptr;
|
|
int o2pressure_sensor = 0;
|
|
std::vector<std::string> converted_strings;
|
|
size_t act_converted_string = 0;
|
|
};
|
|
|
|
struct keyword_action {
|
|
const char *keyword;
|
|
void (*fn)(char *, struct git_parser_state *);
|
|
};
|
|
|
|
static git_blob *git_tree_entry_blob(git_repository *repo, const git_tree_entry *entry);
|
|
|
|
static temperature_t get_temperature(const char *line)
|
|
{
|
|
temperature_t t;
|
|
t.mkelvin = C_to_mkelvin(ascii_strtod(line, NULL));
|
|
return t;
|
|
}
|
|
|
|
static depth_t get_depth(const char *line)
|
|
{
|
|
depth_t d;
|
|
d.mm = lrint(1000 * ascii_strtod(line, NULL));
|
|
return d;
|
|
}
|
|
|
|
static volume_t get_volume(const char *line)
|
|
{
|
|
volume_t v;
|
|
v.mliter = lrint(1000 * ascii_strtod(line, NULL));
|
|
return v;
|
|
}
|
|
|
|
static weight_t get_weight(const char *line)
|
|
{
|
|
weight_t w;
|
|
w.grams = lrint(1000 * ascii_strtod(line, NULL));
|
|
return w;
|
|
}
|
|
|
|
static pressure_t get_airpressure(const char *line)
|
|
{
|
|
return pressure_t { .mbar = int_cast<int32_t>(ascii_strtod(line, NULL)) };
|
|
}
|
|
|
|
static pressure_t get_pressure(const char *line)
|
|
{
|
|
return pressure_t { .mbar = int_cast<int32_t>(1000 * ascii_strtod(line, NULL)) };
|
|
}
|
|
|
|
static o2pressure_t get_o2pressure(const char *line)
|
|
{
|
|
return o2pressure_t { .mbar = int_cast<uint16_t>(1000 * ascii_strtod(line, NULL)) };
|
|
}
|
|
|
|
static int get_salinity(const char *line)
|
|
{
|
|
return lrint(10 * ascii_strtod(line, NULL));
|
|
}
|
|
|
|
static fraction_t get_fraction(const char *line)
|
|
{
|
|
fraction_t f;
|
|
f.permille = lrint(10 * ascii_strtod(line, NULL));
|
|
return f;
|
|
}
|
|
|
|
static void update_date(timestamp_t *when, const char *line)
|
|
{
|
|
unsigned yyyy, mm, dd;
|
|
struct tm tm;
|
|
|
|
if (sscanf(line, "%04u-%02u-%02u", &yyyy, &mm, &dd) != 3)
|
|
return;
|
|
utc_mkdate(*when, &tm);
|
|
tm.tm_year = yyyy;
|
|
tm.tm_mon = mm - 1;
|
|
tm.tm_mday = dd;
|
|
*when = utc_mktime(&tm);
|
|
}
|
|
|
|
static void update_time(timestamp_t *when, const char *line)
|
|
{
|
|
unsigned h, m, s = 0;
|
|
struct tm tm;
|
|
|
|
if (sscanf(line, "%02u:%02u:%02u", &h, &m, &s) < 2)
|
|
return;
|
|
utc_mkdate(*when, &tm);
|
|
tm.tm_hour = h;
|
|
tm.tm_min = m;
|
|
tm.tm_sec = s;
|
|
*when = utc_mktime(&tm);
|
|
}
|
|
|
|
static duration_t get_duration(const char *line)
|
|
{
|
|
int m = 0, s = 0;
|
|
duration_t d;
|
|
sscanf(line, "%d:%d", &m, &s);
|
|
d.seconds = m * 60 + s;
|
|
return d;
|
|
}
|
|
|
|
static enum divemode_t get_dctype(const char *line)
|
|
{
|
|
for (int i = 0; i < NUM_DIVEMODE; i++) {
|
|
if (strcmp(line, divemode_text[i]) == 0)
|
|
return (divemode_t)i;
|
|
}
|
|
return (divemode_t)0;
|
|
}
|
|
|
|
static int get_index(const char *line)
|
|
{ return atoi(line); }
|
|
|
|
static int get_hex(const char *line)
|
|
{ return strtoul(line, NULL, 16); }
|
|
|
|
static void parse_dive_gps(char *line, struct git_parser_state *state)
|
|
{
|
|
location_t location;
|
|
struct dive_site *ds = state->active_dive->dive_site;
|
|
|
|
parse_location(line, &location);
|
|
if (!ds) {
|
|
ds = state->log->sites.get_by_gps(&location);
|
|
if (!ds)
|
|
ds = state->log->sites.create(std::string(), location);
|
|
ds->add_dive(state->active_dive.get());
|
|
} else {
|
|
if (ds->has_gps_location() && ds->location != location) {
|
|
std::string coords = printGPSCoordsC(&location);
|
|
// we have a dive site that already has GPS coordinates
|
|
// note 1: there will be much less copying once the core
|
|
// structures are converted to std::string.
|
|
// note 2: we could include the first newline in the
|
|
// translation string, but that would be weird and cause
|
|
// a new string.
|
|
ds->notes += '\n';
|
|
ds->notes += format_string_std(translate("gettextFromC", "multiple GPS locations for this dive site; also %s\n"), coords.c_str());
|
|
}
|
|
ds->location = location;
|
|
}
|
|
|
|
}
|
|
|
|
// Gets the first converted string and consumes it.
|
|
// Note: does not remove the string from the vector.
|
|
// This is supposed to be used for parsers that expect
|
|
// only one string.
|
|
static std::string get_first_converted_string(struct git_parser_state *state)
|
|
{
|
|
if (state->converted_strings.empty())
|
|
return std::string();
|
|
return std::move(state->converted_strings.front());
|
|
}
|
|
|
|
static void parse_dive_location(char *, struct git_parser_state *state)
|
|
{
|
|
std::string name = get_first_converted_string(state);
|
|
struct dive_site *ds = state->active_dive->dive_site;
|
|
if (!ds) {
|
|
ds = state->log->sites.get_by_name(name);
|
|
if (!ds)
|
|
ds = state->log->sites.create(name);
|
|
ds->add_dive(state->active_dive.get());
|
|
} else {
|
|
// we already had a dive site linked to the dive
|
|
if (ds->name.empty()) {
|
|
ds->name = name.c_str();
|
|
} else {
|
|
// and that dive site had a name. that's weird - if our name is different, add it to the notes
|
|
if (ds->name == name) {
|
|
ds->notes += '\n';
|
|
ds->notes += format_string_std(translate("gettextFromC", "additional name for site: %s\n"), name.c_str());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static void parse_dive_diveguide(char *, struct git_parser_state *state)
|
|
{ state->active_dive->diveguide = get_first_converted_string(state); }
|
|
|
|
static void parse_dive_buddy(char *, struct git_parser_state *state)
|
|
{ state->active_dive->buddy = get_first_converted_string(state); }
|
|
|
|
static void parse_dive_suit(char *, struct git_parser_state *state)
|
|
{ state->active_dive->suit = get_first_converted_string(state); }
|
|
|
|
static void parse_dive_notes(char *, struct git_parser_state *state)
|
|
{ state->active_dive->notes = get_first_converted_string(state); }
|
|
|
|
static void parse_dive_divesiteid(char *line, struct git_parser_state *state)
|
|
{ state->log->sites.get_by_uuid(get_hex(line))->add_dive(state->active_dive.get()); }
|
|
|
|
/*
|
|
* We can have multiple tags.
|
|
*/
|
|
static void parse_dive_tags(char *, struct git_parser_state *state)
|
|
{
|
|
for (const std::string &tag: state->converted_strings) {
|
|
if (!tag.empty())
|
|
taglist_add_tag(state->active_dive->tags, tag.c_str());
|
|
}
|
|
}
|
|
|
|
static void parse_dive_airtemp(char *line, struct git_parser_state *state)
|
|
{ state->active_dive->airtemp = get_temperature(line); }
|
|
|
|
static void parse_dive_watertemp(char *line, struct git_parser_state *state)
|
|
{ state->active_dive->watertemp = get_temperature(line); }
|
|
|
|
static void parse_dive_airpressure(char *line, struct git_parser_state *state)
|
|
{ state->active_dive->surface_pressure = get_airpressure(line); }
|
|
|
|
static void parse_dive_duration(char *line, struct git_parser_state *state)
|
|
{ state->active_dive->duration = get_duration(line); }
|
|
|
|
static void parse_dive_rating(char *line, struct git_parser_state *state)
|
|
{ state->active_dive->rating = get_index(line); }
|
|
|
|
static void parse_dive_visibility(char *line, struct git_parser_state *state)
|
|
{ state->active_dive->visibility = get_index(line); }
|
|
|
|
static void parse_dive_wavesize(char *line, struct git_parser_state *state)
|
|
{ state->active_dive->wavesize = get_index(line); }
|
|
|
|
static void parse_dive_current(char *line, struct git_parser_state *state)
|
|
{ state->active_dive->current = get_index(line); }
|
|
|
|
static void parse_dive_surge(char *line, struct git_parser_state *state)
|
|
{ state->active_dive->surge = get_index(line); }
|
|
|
|
static void parse_dive_chill(char *line, struct git_parser_state *state)
|
|
{ state->active_dive->chill = get_index(line); }
|
|
|
|
static void parse_dive_watersalinity(char *line, struct git_parser_state *state)
|
|
{ state->active_dive->user_salinity = get_salinity(line); }
|
|
|
|
static void parse_dive_notrip(char *, struct git_parser_state *state)
|
|
{
|
|
state->active_dive->notrip = true;
|
|
}
|
|
|
|
static void parse_dive_invalid(char *, struct git_parser_state *state)
|
|
{
|
|
state->active_dive->invalid = true;
|
|
}
|
|
|
|
static void parse_site_description(char *, struct git_parser_state *state)
|
|
{ state->active_site->description = get_first_converted_string(state); }
|
|
|
|
static void parse_site_name(char *, struct git_parser_state *state)
|
|
{ state->active_site->name = get_first_converted_string(state); }
|
|
|
|
static void parse_site_notes(char *, struct git_parser_state *state)
|
|
{ state->active_site->notes = get_first_converted_string(state); }
|
|
|
|
static void parse_site_gps(char *line, struct git_parser_state *state)
|
|
{
|
|
parse_location(line, &state->active_site->location);
|
|
}
|
|
|
|
static void parse_site_geo(char *line, struct git_parser_state *state)
|
|
{
|
|
int origin;
|
|
int category;
|
|
sscanf(line, "cat %d origin %d \"", &category, &origin);
|
|
taxonomy_set_category(state->active_site->taxonomy, (taxonomy_category)category,
|
|
get_first_converted_string(state), (taxonomy_origin)origin);
|
|
}
|
|
|
|
static std::string pop_cstring(struct git_parser_state *state, const char *err)
|
|
{
|
|
if (state->act_converted_string >= state->converted_strings.size()) {
|
|
report_error("git-load: string marker without any strings ('%s')", err);
|
|
return std::string();
|
|
}
|
|
size_t idx = state->act_converted_string++;
|
|
return std::move(state->converted_strings[idx]);
|
|
}
|
|
|
|
/* Parse key=val parts of samples and cylinders etc */
|
|
static char *parse_keyvalue_entry(void (*fn)(void *, const char *, const std::string &), void *fndata, char *line, struct git_parser_state *state)
|
|
{
|
|
char *key = line, c;
|
|
|
|
while ((c = *line) != 0) {
|
|
if (isspace(c) || c == '=')
|
|
break;
|
|
line++;
|
|
}
|
|
|
|
if (c != 0)
|
|
*line++ = 0;
|
|
|
|
std::string val;
|
|
if (c == '=') {
|
|
const char *start_val = line;
|
|
while ((c = *line) != 0) {
|
|
if (isspace(c))
|
|
break;
|
|
line++;
|
|
}
|
|
|
|
/* Did we get a string? Take it from the list of strings */
|
|
val = start_val[0] == '"' ? pop_cstring(state, key)
|
|
: std::string(start_val, line - start_val);
|
|
|
|
if (c)
|
|
line++;
|
|
|
|
}
|
|
|
|
fn(fndata, key, val);
|
|
return line;
|
|
}
|
|
|
|
static void parse_cylinder_keyvalue(void *_cylinder, const char *key, const std::string &value)
|
|
{
|
|
cylinder_t *cylinder = (cylinder_t *)_cylinder;
|
|
if (!strcmp(key, "vol")) {
|
|
cylinder->type.size = get_volume(value.c_str());
|
|
return;
|
|
}
|
|
if (!strcmp(key, "workpressure")) {
|
|
cylinder->type.workingpressure = get_pressure(value.c_str());
|
|
return;
|
|
}
|
|
if (!strcmp(key, "description")) {
|
|
cylinder->type.description = value;
|
|
return;
|
|
}
|
|
if (!strcmp(key, "o2")) {
|
|
cylinder->gasmix.o2 = get_fraction(value.c_str());
|
|
return;
|
|
}
|
|
if (!strcmp(key, "he")) {
|
|
cylinder->gasmix.he = get_fraction(value.c_str());
|
|
return;
|
|
}
|
|
if (!strcmp(key, "start")) {
|
|
cylinder->start = get_pressure(value.c_str());
|
|
return;
|
|
}
|
|
if (!strcmp(key, "end")) {
|
|
cylinder->end = get_pressure(value.c_str());
|
|
return;
|
|
}
|
|
if (!strcmp(key, "use")) {
|
|
cylinder->cylinder_use = cylinderuse_from_text(value.c_str());
|
|
return;
|
|
}
|
|
if (!strcmp(key, "depth")) {
|
|
cylinder->depth = get_depth(value.c_str());
|
|
return;
|
|
}
|
|
if ((*key == 'm') && value.empty()) {
|
|
/* found a bogus key/value pair in the cylinder, consisting
|
|
* of a lonely "m" or m<single quote> without value. This
|
|
* is caused by commit 46004c39e26 and fixed in 48d9c8eb6eb0 and
|
|
* b984fb98c38e4. See referenced commits for more info.
|
|
*
|
|
* Just ignore this key/value pair. No processing is broken
|
|
* due to this, as the git storage stores only metric SI type data.
|
|
* In fact, the m unit is superfluous anyway.
|
|
*/
|
|
return;
|
|
}
|
|
report_error("Unknown cylinder key/value pair (%s/%s)", key, value.c_str());
|
|
}
|
|
|
|
static void parse_dive_cylinder(char *line, struct git_parser_state *state)
|
|
{
|
|
cylinder_t cylinder;
|
|
|
|
for (;;) {
|
|
char c;
|
|
while (isspace(c = *line))
|
|
line++;
|
|
if (!c)
|
|
break;
|
|
line = parse_keyvalue_entry(parse_cylinder_keyvalue, &cylinder, line, state);
|
|
}
|
|
if (cylinder.cylinder_use == OXYGEN)
|
|
state->o2pressure_sensor = static_cast<int>(state->active_dive->cylinders.size());
|
|
|
|
state->active_dive->cylinders.push_back(std::move(cylinder));
|
|
}
|
|
|
|
static void parse_weightsystem_keyvalue(void *_ws, const char *key, const std::string &value)
|
|
{
|
|
weightsystem_t *ws = (weightsystem_t *)_ws;
|
|
if (!strcmp(key, "weight")) {
|
|
ws->weight = get_weight(value.c_str());
|
|
return;
|
|
}
|
|
if (!strcmp(key, "description")) {
|
|
ws->description = value;
|
|
return;
|
|
}
|
|
report_error("Unknown weightsystem key/value pair (%s/%s)", key, value.c_str());
|
|
}
|
|
|
|
static void parse_dive_weightsystem(char *line, struct git_parser_state *state)
|
|
{
|
|
weightsystem_t ws;
|
|
|
|
for (;;) {
|
|
char c;
|
|
while (isspace(c = *line))
|
|
line++;
|
|
if (!c)
|
|
break;
|
|
line = parse_keyvalue_entry(parse_weightsystem_keyvalue, &ws, line, state);
|
|
}
|
|
|
|
state->active_dive->weightsystems.push_back(std::move(ws));
|
|
}
|
|
|
|
static int match_action(char *line, void *data,
|
|
const struct keyword_action *action, unsigned nr_action)
|
|
{
|
|
char *p = line, c;
|
|
unsigned low, high;
|
|
|
|
while ((c = *p) >= 'a' && c <= 'z') // skip over 1st word
|
|
p++; // Extract the second word from the line:
|
|
if (p == line)
|
|
return -1;
|
|
switch (c) {
|
|
case 0: // if 2nd word is C-terminated
|
|
break;
|
|
case ' ': // =end of 2nd word?
|
|
*p++ = 0; // then C-terminate that word
|
|
break;
|
|
default:
|
|
return -1;
|
|
}
|
|
|
|
/* Standard binary search in a table */
|
|
low = 0;
|
|
high = nr_action;
|
|
while (low < high) {
|
|
unsigned mid = (low + high)/2;
|
|
const struct keyword_action *a = action + mid;
|
|
int cmp = strcmp(line, a->keyword);
|
|
if (!cmp) { // attribute found:
|
|
a->fn(p, (git_parser_state *)data); // Execute appropriate function,
|
|
return 0; // .. passing 2n word from above
|
|
} // (p) as a function argument.
|
|
if (cmp < 0)
|
|
high = mid;
|
|
else
|
|
low = mid + 1;
|
|
}
|
|
report_error("Unmatched action '%s'", line);
|
|
return -1;
|
|
}
|
|
|
|
template<size_t N>
|
|
static int match_action(char *line, void *data, const std::array<keyword_action, N> &action)
|
|
{
|
|
return match_action(line, data, &action[0], N);
|
|
}
|
|
|
|
/* FIXME! We should do the array thing here too. */
|
|
static void parse_sample_keyvalue(void *_sample, const char *key, const std::string &value)
|
|
{
|
|
struct sample *sample = (struct sample *)_sample;
|
|
|
|
if (!strcmp(key, "sensor")) {
|
|
sample->sensor[0] = atoi(value.c_str());
|
|
return;
|
|
}
|
|
if (!strcmp(key, "ndl")) {
|
|
sample->ndl = get_duration(value.c_str());
|
|
return;
|
|
}
|
|
if (!strcmp(key, "tts")) {
|
|
sample->tts = get_duration(value.c_str());
|
|
return;
|
|
}
|
|
if (!strcmp(key, "in_deco")) {
|
|
sample->in_deco = atoi(value.c_str());
|
|
return;
|
|
}
|
|
if (!strcmp(key, "stoptime")) {
|
|
sample->stoptime = get_duration(value.c_str());
|
|
return;
|
|
}
|
|
if (!strcmp(key, "stopdepth")) {
|
|
sample->stopdepth = get_depth(value.c_str());
|
|
return;
|
|
}
|
|
if (!strcmp(key, "cns")) {
|
|
sample->cns = atoi(value.c_str());
|
|
return;
|
|
}
|
|
|
|
if (!strcmp(key, "rbt")) {
|
|
sample->rbt = get_duration(value.c_str());
|
|
return;
|
|
}
|
|
|
|
if (!strcmp(key, "po2")) {
|
|
sample->setpoint = get_o2pressure(value.c_str());
|
|
return;
|
|
}
|
|
if (!strcmp(key, "sensor1")) {
|
|
sample->o2sensor[0] = get_o2pressure(value.c_str());
|
|
return;
|
|
}
|
|
if (!strcmp(key, "sensor2")) {
|
|
sample->o2sensor[1] = get_o2pressure(value.c_str());
|
|
return;
|
|
}
|
|
if (!strcmp(key, "sensor3")) {
|
|
sample->o2sensor[2] = get_o2pressure(value.c_str());
|
|
return;
|
|
}
|
|
if (!strcmp(key, "sensor4")) {
|
|
sample->o2sensor[3] = get_o2pressure(value.c_str());
|
|
return;
|
|
}
|
|
if (!strcmp(key, "sensor5")) {
|
|
sample->o2sensor[4] = get_o2pressure(value.c_str());
|
|
return;
|
|
}
|
|
if (!strcmp(key, "sensor6")) {
|
|
sample->o2sensor[5] = get_o2pressure(value.c_str());
|
|
return;
|
|
}
|
|
if (!strcmp(key, "o2pressure")) {
|
|
sample->pressure[1] = get_pressure(value.c_str());
|
|
return;
|
|
}
|
|
if (!strcmp(key, "heartbeat")) {
|
|
sample->heartbeat = atoi(value.c_str());
|
|
return;
|
|
}
|
|
if (!strcmp(key, "bearing")) {
|
|
sample->bearing.degrees = atoi(value.c_str());
|
|
return;
|
|
}
|
|
|
|
report_error("Unexpected sample key/value pair (%s/%s)", key, value.c_str());
|
|
}
|
|
|
|
static char *parse_sample_unit(struct sample *sample, double val, char *unit)
|
|
{
|
|
unsigned int sensor;
|
|
char *end = unit, c;
|
|
|
|
/* Skip over the unit */
|
|
while ((c = *end) != 0) {
|
|
if (isspace(c)) {
|
|
*end++ = 0;
|
|
break;
|
|
}
|
|
end++;
|
|
}
|
|
|
|
/* The units are "°C", "m" or "bar", so let's just look at the first character */
|
|
/* The cylinder pressure may also be of the form '123.0bar:4' to indicate sensor */
|
|
switch (*unit) {
|
|
case 'm':
|
|
sample->depth.mm = lrint(1000 * val);
|
|
break;
|
|
case 'b':
|
|
sensor = sample->sensor[0];
|
|
if (end > unit + 4 && unit[3] == ':')
|
|
sensor = atoi(unit + 4);
|
|
add_sample_pressure(sample, sensor, lrint(1000 * val));
|
|
break;
|
|
default:
|
|
sample->temperature.mkelvin = C_to_mkelvin(val);
|
|
break;
|
|
}
|
|
|
|
return end;
|
|
}
|
|
|
|
/*
|
|
* If the given cylinder doesn't exist, return NO_SENSOR.
|
|
*/
|
|
static int sanitize_sensor_id(const struct dive *d, int nr)
|
|
{
|
|
return d && nr >= 0 && static_cast<size_t>(nr) < d->cylinders.size() ? nr : NO_SENSOR;
|
|
}
|
|
|
|
/*
|
|
* By default the sample data does not change unless the
|
|
* save-file gives an explicit new value. So we copy the
|
|
* data from the previous sample if one exists, and then
|
|
* the parsing will update it as necessary.
|
|
*
|
|
* There are a few exceptions, like the sample pressure:
|
|
* missing sample pressure doesn't mean "same as last
|
|
* time", but "interpolate". We clear those ones
|
|
* explicitly.
|
|
*
|
|
* NOTE! We default sensor use to 0, 1 respetively for
|
|
* the two sensors, but for CCR dives with explicit
|
|
* OXYGEN bottles we set the secondary sensor to that.
|
|
* Then the primary sensor will be either the first
|
|
* or the second cylinder depending on what isn't an
|
|
* oxygen cylinder.
|
|
*/
|
|
static struct sample *new_sample(struct git_parser_state *state)
|
|
{
|
|
struct sample *sample = prepare_sample(state->active_dc);
|
|
size_t num_samples = state->active_dc->samples.size();
|
|
if (num_samples >= 2) {
|
|
*sample = state->active_dc->samples[num_samples - 2];
|
|
sample->pressure[0] = 0_bar;
|
|
sample->pressure[1] = 0_bar;
|
|
} else {
|
|
sample->sensor[0] = sanitize_sensor_id(state->active_dive.get(), !state->o2pressure_sensor);
|
|
sample->sensor[1] = sanitize_sensor_id(state->active_dive.get(), state->o2pressure_sensor);
|
|
}
|
|
return sample;
|
|
}
|
|
|
|
static void sample_parser(char *line, struct git_parser_state *state)
|
|
{
|
|
int m, s = 0;
|
|
struct sample *sample = new_sample(state);
|
|
|
|
m = strtol(line, &line, 10);
|
|
if (*line == ':')
|
|
s = strtol(line + 1, &line, 10);
|
|
sample->time.seconds = m * 60 + s;
|
|
|
|
for (;;) {
|
|
char c;
|
|
|
|
while (isspace(c = *line))
|
|
line++;
|
|
if (!c)
|
|
break;
|
|
/* Less common sample entries have a name */
|
|
if (c >= 'a' && c <= 'z') {
|
|
line = parse_keyvalue_entry(parse_sample_keyvalue, sample, line, state);
|
|
} else {
|
|
const char *end;
|
|
double val = ascii_strtod(line, &end);
|
|
if (end == line) {
|
|
report_error("Odd sample data: %s", line);
|
|
break;
|
|
}
|
|
line = (char *)end;
|
|
line = parse_sample_unit(sample, val, line);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void parse_dc_airtemp(char *line, struct git_parser_state *state)
|
|
{ state->active_dc->airtemp = get_temperature(line); }
|
|
|
|
static void parse_dc_date(char *line, struct git_parser_state *state)
|
|
{ update_date(&state->active_dc->when, line); }
|
|
|
|
static void parse_dc_deviceid(char *line, struct git_parser_state *state)
|
|
{
|
|
get_hex(line); // legacy
|
|
}
|
|
|
|
static void parse_dc_diveid(char *line, struct git_parser_state *state)
|
|
{ state->active_dc->diveid = get_hex(line); }
|
|
|
|
static void parse_dc_duration(char *line, struct git_parser_state *state)
|
|
{ state->active_dc->duration = get_duration(line); }
|
|
|
|
static void parse_dc_dctype(char *line, struct git_parser_state *state)
|
|
{ state->active_dc->divemode = get_dctype(line); }
|
|
|
|
static void parse_dc_lastmanualtime(char *line, struct git_parser_state *state)
|
|
{ state->active_dc->last_manual_time = get_duration(line); }
|
|
|
|
static void parse_dc_maxdepth(char *line, struct git_parser_state *state)
|
|
{ state->active_dc->maxdepth = get_depth(line); }
|
|
|
|
static void parse_dc_meandepth(char *line, struct git_parser_state *state)
|
|
{ state->active_dc->meandepth = get_depth(line); }
|
|
|
|
static void parse_dc_model(char *, struct git_parser_state *state)
|
|
{ state->active_dc->model = get_first_converted_string(state); }
|
|
|
|
static void parse_dc_numberofoxygensensors(char *line, struct git_parser_state *state)
|
|
{ state->active_dc->no_o2sensors = get_index(line); }
|
|
|
|
static void parse_dc_surfacepressure(char *line, struct git_parser_state *state)
|
|
{ state->active_dc->surface_pressure = get_pressure(line); }
|
|
|
|
static void parse_dc_salinity(char *line, struct git_parser_state *state)
|
|
{ state->active_dc->salinity = get_salinity(line); }
|
|
|
|
static void parse_dc_surfacetime(char *line, struct git_parser_state *state)
|
|
{ state->active_dc->surfacetime = get_duration(line); }
|
|
|
|
static void parse_dc_time(char *line, struct git_parser_state *state)
|
|
{ update_time(&state->active_dc->when, line); }
|
|
|
|
static void parse_dc_watertemp(char *line, struct git_parser_state *state)
|
|
{ state->active_dc->watertemp = get_temperature(line); }
|
|
|
|
|
|
static int get_divemode(const char *divemodestring) {
|
|
for (int i = 0; i < NUM_DIVEMODE; i++) {
|
|
if (!strcmp(divemodestring, divemode_text[i]))
|
|
return i;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* A 'struct event' has a variable-sized name allocation at the
|
|
* end. So when we parse the event data, we can't fill in the
|
|
* event directly, because we don't know how to allocate one
|
|
* before we have the size of the name.
|
|
*
|
|
* Thus this initial 'parse_event' with a separate name pointer.
|
|
*/
|
|
struct parse_event {
|
|
std::string name;
|
|
int has_divemode = false;
|
|
struct event ev;
|
|
};
|
|
|
|
static void parse_event_keyvalue(void *_parse, const char *key, const std::string &value)
|
|
{
|
|
struct parse_event *parse = (parse_event *)_parse;
|
|
int val = atoi(value.c_str());
|
|
|
|
if (!strcmp(key, "type")) {
|
|
parse->ev.type = val;
|
|
} else if (!strcmp(key, "flags")) {
|
|
parse->ev.flags = val;
|
|
} else if (!strcmp(key, "value")) {
|
|
parse->ev.value = val;
|
|
} else if (!strcmp(key, "name")) {
|
|
parse->name = value;
|
|
} else if (!strcmp(key,"divemode")) {
|
|
parse->ev.value = get_divemode(value.c_str());
|
|
parse->has_divemode = 1;
|
|
} else if (!strcmp(key, "cylinder")) {
|
|
/* NOTE! We add one here as a marker that "yes, we got a cylinder index" */
|
|
parse->ev.gas.index = 1 + get_index(value.c_str());
|
|
} else if (!strcmp(key, "o2")) {
|
|
parse->ev.gas.mix.o2 = get_fraction(value.c_str());
|
|
} else if (!strcmp(key, "he")) {
|
|
parse->ev.gas.mix.he = get_fraction(value.c_str());
|
|
} else
|
|
report_error("Unexpected event key/value pair (%s/%s)", key, value.c_str());
|
|
}
|
|
|
|
/* keyvalue "key" "value"
|
|
* so we have two strings (possibly empty) */
|
|
static void parse_dc_keyvalue(char *line, struct git_parser_state *state)
|
|
{
|
|
// Let's make sure we have two strings...
|
|
if (state->converted_strings.size() != 2)
|
|
return;
|
|
|
|
add_extra_data(state->active_dc, state->converted_strings[0], state->converted_strings[1]);
|
|
}
|
|
|
|
static void parse_dc_event(char *line, struct git_parser_state *state)
|
|
{
|
|
int m, s = 0;
|
|
struct parse_event p;
|
|
|
|
m = strtol(line, &line, 10);
|
|
if (*line == ':')
|
|
s = strtol(line + 1, &line, 10);
|
|
p.ev.time.seconds = m * 60 + s;
|
|
|
|
for (;;) {
|
|
char c;
|
|
while (isspace(c = *line))
|
|
line++;
|
|
if (!c)
|
|
break;
|
|
line = parse_keyvalue_entry(parse_event_keyvalue, &p, line, state);
|
|
}
|
|
|
|
/* Only modechange events should have a divemode - fix up any corrupted names */
|
|
if (p.has_divemode && p.name != "modechange")
|
|
p.name = "modechange";
|
|
|
|
struct event *ev = add_event(state->active_dc, p.ev.time.seconds, p.ev.type, p.ev.flags, p.ev.value, p.name.c_str());
|
|
|
|
/*
|
|
* Older logs might mark the dive to be CCR by having an "SP change" event at time 0:00.
|
|
* Better to mark them being CCR on import so no need for special treatments elsewhere on
|
|
* the code.
|
|
*/
|
|
if (p.ev.time.seconds == 0 && p.ev.type == SAMPLE_EVENT_PO2 && p.ev.value && state->active_dc->divemode==OC)
|
|
state->active_dc->divemode = CCR;
|
|
|
|
if (ev->is_gaschange()) {
|
|
/*
|
|
* We subtract one here because "0" is "no index",
|
|
* and the parsing will add one for actual cylinder
|
|
* index data (see parse_event_keyvalue)
|
|
*/
|
|
ev->gas.index = p.ev.gas.index-1;
|
|
if (p.ev.gas.mix.o2.permille || p.ev.gas.mix.he.permille)
|
|
ev->gas.mix = p.ev.gas.mix;
|
|
}
|
|
}
|
|
|
|
/* Not needed anymore - trip date calculated implicitly from first dive */
|
|
static void parse_trip_date(char *, struct git_parser_state *)
|
|
{ }
|
|
|
|
/* Not needed anymore - trip date calculated implicitly from first dive */
|
|
static void parse_trip_time(char *, struct git_parser_state *)
|
|
{ }
|
|
|
|
static void parse_trip_location(char *, struct git_parser_state *state)
|
|
{ state->active_trip->location = get_first_converted_string(state); }
|
|
|
|
static void parse_trip_notes(char *, struct git_parser_state *state)
|
|
{ state->active_trip->notes = get_first_converted_string(state); }
|
|
|
|
static void parse_settings_autogroup(char *, struct git_parser_state *state)
|
|
{
|
|
state->log->autogroup = true;
|
|
}
|
|
|
|
static void parse_settings_units(char *line, struct git_parser_state *)
|
|
{
|
|
if (line)
|
|
set_informational_units(line);
|
|
}
|
|
|
|
static void parse_settings_userid(char *, struct git_parser_state *)
|
|
/* Keep this despite removal of the webservice as there are legacy logbook around
|
|
* that still have this defined.
|
|
*/
|
|
{
|
|
}
|
|
|
|
static void parse_settings_prefs(char *line, struct git_parser_state *)
|
|
{
|
|
if (line)
|
|
set_git_prefs(line);
|
|
}
|
|
|
|
/*
|
|
* Our versioning is a joke right now, but this is more of an example of what we
|
|
* *can* do some day. And if we do change the version, this warning will show if
|
|
* you read with a version of subsurface that doesn't know about it.
|
|
* We MUST keep this in sync with the XML version (so we can report a consistent
|
|
* minimum datafile version)
|
|
*/
|
|
static void parse_settings_version(char *line, struct git_parser_state *)
|
|
{
|
|
int version = atoi(line);
|
|
report_datafile_version(version);
|
|
if (version > dataformat_version)
|
|
report_error("Git save file version %d is newer than version %d I know about", version, dataformat_version);
|
|
}
|
|
|
|
/* The argument string is the version string of subsurface that saved things, just FYI */
|
|
static void parse_settings_subsurface(char *, struct git_parser_state *)
|
|
{
|
|
}
|
|
|
|
struct divecomputerid {
|
|
std::string model;
|
|
std::string nickname;
|
|
std::string serial;
|
|
unsigned int deviceid = 0;
|
|
};
|
|
|
|
static void parse_divecomputerid_keyvalue(void *_cid, const char *key, const std::string &value)
|
|
{
|
|
struct divecomputerid *cid = (divecomputerid *)_cid;
|
|
|
|
// Ignored legacy fields
|
|
if (!strcmp(key, "firmware"))
|
|
return;
|
|
if (!strcmp(key, "deviceid"))
|
|
return;
|
|
|
|
// Serial number and nickname matter
|
|
if (!strcmp(key, "serial")) {
|
|
cid->serial = value;
|
|
cid->deviceid = calculate_string_hash(value.c_str());
|
|
return;
|
|
}
|
|
if (!strcmp(key, "nickname")) {
|
|
cid->nickname = value;
|
|
return;
|
|
}
|
|
report_error("Unknown divecomputerid key/value pair (%s/%s)", key, value.c_str());
|
|
}
|
|
|
|
/*
|
|
* The 'divecomputerid' is a bit harder to parse than some other things, because
|
|
* it can have multiple strings (but see the tag parsing for another example of
|
|
* that) in addition to the non-string entries.
|
|
*/
|
|
static void parse_settings_divecomputerid(char *line, struct git_parser_state *state)
|
|
{
|
|
struct divecomputerid id;
|
|
id.model = pop_cstring(state, line);
|
|
|
|
/* Skip the '"' that stood for the model string */
|
|
line++;
|
|
|
|
/* Parse the rest of the entries */
|
|
for (;;) {
|
|
char c;
|
|
while (isspace(c = *line))
|
|
line++;
|
|
if (!c)
|
|
break;
|
|
line = parse_keyvalue_entry(parse_divecomputerid_keyvalue, &id, line, state);
|
|
}
|
|
create_device_node(state->log->devices, id.model.c_str(), id.serial.c_str(), id.nickname.c_str());
|
|
}
|
|
|
|
struct fingerprint_helper {
|
|
uint32_t model = 0;
|
|
uint32_t serial = 0;
|
|
uint32_t fdeviceid = 0;
|
|
uint32_t fdiveid = 0;
|
|
std::string hex_data;
|
|
};
|
|
|
|
static void parse_fingerprint_keyvalue(void *_fph, const char *key, const std::string &value)
|
|
{
|
|
struct fingerprint_helper *fph = (fingerprint_helper *)_fph;
|
|
|
|
if (!strcmp(key, "model")) {
|
|
fph->model = get_hex(value.c_str());
|
|
return;
|
|
}
|
|
if (!strcmp(key, "serial")) {
|
|
fph->serial = get_hex(value.c_str());
|
|
return;
|
|
}
|
|
if (!strcmp(key, "deviceid")) {
|
|
fph->fdeviceid = get_hex(value.c_str());
|
|
return;
|
|
}
|
|
if (!strcmp(key, "diveid")) {
|
|
fph->fdiveid = get_hex(value.c_str());
|
|
return;
|
|
}
|
|
if (!strcmp(key, "data")) {
|
|
fph->hex_data = value.c_str();
|
|
return;
|
|
}
|
|
report_error("Unknown fingerprint key/value pair (%s/%s)", key, value.c_str());
|
|
}
|
|
|
|
|
|
static void parse_settings_fingerprint(char *line, struct git_parser_state *state)
|
|
{
|
|
struct fingerprint_helper fph;
|
|
for (;;) {
|
|
char c;
|
|
while (isspace(c = *line))
|
|
line++;
|
|
if (!c)
|
|
break;
|
|
line = parse_keyvalue_entry(parse_fingerprint_keyvalue, &fph, line, state);
|
|
}
|
|
if (verbose > 1)
|
|
report_info("fingerprint %08x %08x %08x %08x %s\n", fph.model, fph.serial, fph.fdeviceid, fph.fdiveid, fph.hex_data.c_str());
|
|
create_fingerprint_node_from_hex(fingerprints, fph.model, fph.serial,
|
|
fph.hex_data, fph.fdeviceid, fph.fdiveid);
|
|
}
|
|
|
|
static void parse_picture_filename(char *, struct git_parser_state *state)
|
|
{
|
|
state->active_pic.filename = get_first_converted_string(state);
|
|
}
|
|
|
|
static void parse_picture_gps(char *line, struct git_parser_state *state)
|
|
{
|
|
parse_location(line, &state->active_pic.location);
|
|
}
|
|
|
|
static void parse_picture_hash(char *, struct git_parser_state *)
|
|
{
|
|
// we no longer use hashes to identify pictures, but we shouldn't
|
|
// remove this parser lest users get an ugly red warning when
|
|
// opening old git repos
|
|
}
|
|
|
|
/* These need to be sorted! */
|
|
static const std::array dc_action {
|
|
#undef D
|
|
#define D(x) keyword_action { #x, parse_dc_ ## x }
|
|
D(airtemp), D(date), D(dctype), D(deviceid), D(diveid), D(duration),
|
|
D(event), D(keyvalue), D(lastmanualtime), D(maxdepth), D(meandepth), D(model), D(numberofoxygensensors),
|
|
D(salinity), D(surfacepressure), D(surfacetime), D(time), D(watertemp)
|
|
};
|
|
|
|
/* Sample lines start with a space or a number */
|
|
static void divecomputer_parser(char *line, struct git_parser_state *state)
|
|
{
|
|
char c = *line;
|
|
if (c < 'a' || c > 'z')
|
|
sample_parser(line, state);
|
|
match_action(line, state, dc_action);
|
|
}
|
|
|
|
/* These need to be sorted! */
|
|
static const std::array dive_action {
|
|
#undef D
|
|
#define D(x) keyword_action { #x, parse_dive_ ## x }
|
|
/* For historical reasons, we accept divemaster and diveguide */
|
|
D(airpressure), D(airtemp), D(buddy), D(chill), D(current), D(cylinder), D(diveguide),
|
|
keyword_action { "divemaster", parse_dive_diveguide },
|
|
D(divesiteid), D(duration), D(gps), D(invalid), D(location), D(notes), D(notrip), D(rating), D(suit), D(surge),
|
|
D(tags), D(visibility), D(watersalinity), D(watertemp), D(wavesize), D(weightsystem)
|
|
};
|
|
|
|
static void dive_parser(char *line, struct git_parser_state *state)
|
|
{
|
|
match_action(line, state, dive_action);
|
|
}
|
|
|
|
/* These need to be sorted! */
|
|
static const std::array site_action {
|
|
#undef D
|
|
#define D(x) keyword_action { #x, parse_site_ ## x }
|
|
D(description), D(geo), D(gps), D(name), D(notes)
|
|
};
|
|
|
|
static void site_parser(char *line, struct git_parser_state *state)
|
|
{
|
|
match_action(line, state, site_action);
|
|
}
|
|
|
|
/* These need to be sorted! */
|
|
static const std::array trip_action {
|
|
#undef D
|
|
#define D(x) keyword_action { #x, parse_trip_ ## x }
|
|
D(date), D(location), D(notes), D(time),
|
|
};
|
|
|
|
static void trip_parser(char *line, struct git_parser_state *state)
|
|
{
|
|
match_action(line, state, trip_action);
|
|
}
|
|
|
|
/* These need to be sorted! */
|
|
static const std::array settings_action {
|
|
#undef D
|
|
#define D(x) keyword_action { #x, parse_settings_ ## x }
|
|
D(autogroup), D(divecomputerid), D(fingerprint), D(prefs), D(subsurface), D(units), D(userid), D(version)
|
|
};
|
|
|
|
static void settings_parser(char *line, struct git_parser_state *state)
|
|
{
|
|
match_action(line, state, settings_action);
|
|
}
|
|
|
|
/* These need to be sorted! */
|
|
static const std::array picture_action {
|
|
#undef D
|
|
#define D(x) keyword_action { #x, parse_picture_ ## x }
|
|
D(filename), D(gps), D(hash)
|
|
};
|
|
|
|
static void picture_parser(char *line, struct git_parser_state *state)
|
|
{
|
|
match_action(line, state, picture_action);
|
|
}
|
|
|
|
static void parse_filter_preset_constraint_keyvalue(void *_state, const char *key, const std::string &value)
|
|
{
|
|
struct git_parser_state *state = (git_parser_state *)_state;
|
|
if (!strcmp(key, "type")) {
|
|
state->filter_constraint_type = value;
|
|
return;
|
|
}
|
|
if (!strcmp(key, "rangemode")) {
|
|
state->filter_constraint_range_mode = value;
|
|
return;
|
|
}
|
|
if (!strcmp(key, "stringmode")) {
|
|
state->filter_constraint_string_mode = value;
|
|
return;
|
|
}
|
|
if (!strcmp(key, "negate")) {
|
|
state->filter_constraint_negate = true;
|
|
return;
|
|
}
|
|
if (!strcmp(key, "data")) {
|
|
state->filter_constraint_data = value;
|
|
return;
|
|
}
|
|
|
|
report_error("Unknown filter preset constraint key/value pair (%s/%s)", key, value.c_str());
|
|
}
|
|
|
|
static void parse_filter_preset_constraint(char *line, struct git_parser_state *state)
|
|
{
|
|
for (;;) {
|
|
char c;
|
|
while (isspace(c = *line))
|
|
line++;
|
|
if (!c)
|
|
break;
|
|
line = parse_keyvalue_entry(parse_filter_preset_constraint_keyvalue, state, line, state);
|
|
}
|
|
|
|
state->active_filter->add_constraint(state->filter_constraint_type,
|
|
state->filter_constraint_string_mode,
|
|
state->filter_constraint_range_mode,
|
|
state->filter_constraint_negate, state->filter_constraint_data);
|
|
state->filter_constraint_type.clear();
|
|
state->filter_constraint_string_mode.clear();
|
|
state->filter_constraint_range_mode.clear();
|
|
state->filter_constraint_negate = false;
|
|
state->filter_constraint_data.clear();
|
|
}
|
|
|
|
static void parse_filter_preset_fulltext_keyvalue(void *_state, const char *key, const std::string &value)
|
|
{
|
|
struct git_parser_state *state = (git_parser_state *)_state;
|
|
if (!strcmp(key, "mode")) {
|
|
state->fulltext_mode = value;
|
|
return;
|
|
}
|
|
if (!strcmp(key, "query")) {
|
|
state->fulltext_query = value;
|
|
return;
|
|
}
|
|
|
|
report_error("Unknown filter preset fulltext key/value pair (%s/%s)", key, value.c_str());
|
|
}
|
|
|
|
static void parse_filter_preset_fulltext(char *line, struct git_parser_state *state)
|
|
{
|
|
for (;;) {
|
|
char c;
|
|
while (isspace(c = *line))
|
|
line++;
|
|
if (!c)
|
|
break;
|
|
line = parse_keyvalue_entry(parse_filter_preset_fulltext_keyvalue, state, line, state);
|
|
}
|
|
|
|
state->active_filter.get()->set_fulltext(std::move(state->fulltext_query), state->fulltext_mode);
|
|
state->fulltext_mode.clear();
|
|
state->fulltext_query.clear();
|
|
}
|
|
|
|
static void parse_filter_preset_name(char *, struct git_parser_state *state)
|
|
{
|
|
state->active_filter->name = get_first_converted_string(state);
|
|
}
|
|
|
|
/* These need to be sorted! */
|
|
const std::array filter_preset_action {
|
|
#undef D
|
|
#define D(x) keyword_action { #x, parse_filter_preset_ ## x }
|
|
D(constraint), D(fulltext), D(name)
|
|
};
|
|
|
|
static void filter_preset_parser(char *line, struct git_parser_state *state)
|
|
{
|
|
match_action(line, state, filter_preset_action);
|
|
}
|
|
|
|
/*
|
|
* We have a very simple line-based interface, with the small
|
|
* complication that lines can have strings in the middle, and
|
|
* a string can be multiple lines.
|
|
*
|
|
* The UTF-8 string escaping is *very* simple, though:
|
|
*
|
|
* - a string starts and ends with double quotes (")
|
|
*
|
|
* - inside the string we escape:
|
|
* (a) double quotes with '\"'
|
|
* (b) backslash (\) with '\\'
|
|
*
|
|
* - additionally, for human readability, we escape
|
|
* newlines with '\n\t', with the exception that
|
|
* consecutive newlines are left unescaped (so an
|
|
* empty line doesn't become a line with just a tab
|
|
* on it).
|
|
*
|
|
* Also, while the UTF-8 string can have arbitrarily
|
|
* long lines, the non-string parts of the lines are
|
|
* never long, so we can use a small temporary buffer
|
|
* on stack for that part.
|
|
*
|
|
* Also, note that if a line has one or more strings
|
|
* in it:
|
|
*
|
|
* - each string will be represented as a single '"'
|
|
* character in the output.
|
|
*
|
|
* - all string will be stores in converted_strings.
|
|
*/
|
|
static const char *parse_one_string(const char *buf, const char *end, std::vector<std::string> &converted_strings)
|
|
{
|
|
const char *p = buf;
|
|
|
|
/*
|
|
* We turn multiple strings one one line (think dive tags) into the
|
|
* converted_strings vector.
|
|
*/
|
|
|
|
std::string s;
|
|
while (p < end) {
|
|
char replace;
|
|
|
|
switch (*p++) {
|
|
default:
|
|
continue;
|
|
case '\n':
|
|
if (p < end && *p == '\t') {
|
|
replace = '\n';
|
|
break;
|
|
}
|
|
continue;
|
|
case '\\':
|
|
if (p < end) {
|
|
replace = *p;
|
|
break;
|
|
}
|
|
continue;
|
|
case '"':
|
|
replace = 0;
|
|
break;
|
|
}
|
|
s.append(buf, p - buf - 1);
|
|
if (!replace)
|
|
break;
|
|
s += replace;
|
|
buf = ++p;
|
|
}
|
|
converted_strings.push_back(std::move(s));
|
|
return p;
|
|
}
|
|
|
|
typedef void (line_fn_t)(char *, struct git_parser_state *);
|
|
#define MAXLINE 500
|
|
static unsigned parse_one_line(const char *buf, unsigned size, line_fn_t *fn, struct git_parser_state *state)
|
|
{
|
|
const char *end = buf + size;
|
|
const char *p = buf;
|
|
char line[MAXLINE + 1];
|
|
int off = 0;
|
|
|
|
// Check the first character of a line: an empty line
|
|
// or a line starting with a TAB is invalid, and likely
|
|
// due to an early string end quote due to a merge
|
|
// conflict. Ignore such a line.
|
|
switch (*p) {
|
|
case '\n': case '\t':
|
|
do {
|
|
if (*p++ == '\n')
|
|
break;
|
|
} while (p < end);
|
|
report_info("git storage: Ignoring line '%.*s'", (int)(p-buf-1), buf);
|
|
return p - buf;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
while (p < end) {
|
|
char c = *p++;
|
|
if (c == '\n')
|
|
break;
|
|
line[off] = c;
|
|
off++;
|
|
if (off > MAXLINE)
|
|
off = MAXLINE;
|
|
if (c == '"')
|
|
p = parse_one_string(p, end, state->converted_strings);
|
|
}
|
|
line[off] = 0;
|
|
fn(line, state);
|
|
return p - buf;
|
|
}
|
|
|
|
/*
|
|
* We keep on re-using the vector that stores converted
|
|
* strings, but the callback function can consume the
|
|
* strings.
|
|
*/
|
|
static void for_each_line(git_blob *blob, line_fn_t *fn, struct git_parser_state *state)
|
|
{
|
|
const char *content = (const char *)git_blob_rawcontent(blob);
|
|
unsigned int size = git_blob_rawsize(blob);
|
|
|
|
while (size) {
|
|
state->converted_strings.clear();
|
|
state->act_converted_string = 0;
|
|
unsigned int n = parse_one_line(content, size, fn, state);
|
|
content += n;
|
|
size -= n;
|
|
}
|
|
}
|
|
|
|
#define GIT_WALK_OK 0
|
|
#define GIT_WALK_SKIP 1
|
|
|
|
static void finish_active_trip(struct git_parser_state *state)
|
|
{
|
|
auto &trip = state->active_trip;
|
|
|
|
if (trip)
|
|
state->log->trips.put(std::move(trip));
|
|
}
|
|
|
|
static void finish_active_dive(struct git_parser_state *state)
|
|
{
|
|
if (state->active_dive)
|
|
state->log->dives.record_dive(std::move(state->active_dive));
|
|
}
|
|
|
|
static void create_new_dive(timestamp_t when, struct git_parser_state *state)
|
|
{
|
|
state->active_dive = std::make_unique<dive>();
|
|
|
|
/* We'll fill in more data from the dive file */
|
|
state->active_dive->when = when;
|
|
|
|
if (state->active_trip)
|
|
state->active_trip->add_dive(state->active_dive.get());
|
|
}
|
|
|
|
static bool validate_date(int yyyy, int mm, int dd)
|
|
{
|
|
return yyyy > 1930 && yyyy < 3000 &&
|
|
mm > 0 && mm < 13 &&
|
|
dd > 0 && dd < 32;
|
|
}
|
|
|
|
static bool validate_time(int h, int m, int s)
|
|
{
|
|
return h >= 0 && h < 24 &&
|
|
m >= 0 && m < 60 &&
|
|
s >=0 && s <= 60;
|
|
}
|
|
|
|
/*
|
|
* Dive trip directory, name is 'nn-alphabetic[~hex]'
|
|
*/
|
|
static int dive_trip_directory(const char *root, const char *name, struct git_parser_state *state)
|
|
{
|
|
int yyyy = -1, mm = -1, dd = -1;
|
|
|
|
if (sscanf(root, "%d/%d", &yyyy, &mm) != 2)
|
|
return GIT_WALK_SKIP;
|
|
dd = atoi(name);
|
|
if (!validate_date(yyyy, mm, dd))
|
|
return GIT_WALK_SKIP;
|
|
finish_active_trip(state);
|
|
state->active_trip = std::make_unique<dive_trip>();
|
|
return GIT_WALK_OK;
|
|
}
|
|
|
|
/*
|
|
* Dive directory, name is [[yyyy-]mm-]nn-ddd-hh:mm:ss[~hex] in older git repositories
|
|
* but [[yyyy-]mm-]nn-ddd-hh=mm=ss[~hex] in newer repos as ':' is an illegal character for Windows files
|
|
* and 'timeoff' points to what should be the time part of
|
|
* the name (the first digit of the hour).
|
|
*
|
|
* The root path will be of the form yyyy/mm[/tripdir],
|
|
*/
|
|
static int dive_directory(const char *root, const git_tree_entry *entry, const char *name, int timeoff, struct git_parser_state *state)
|
|
{
|
|
int yyyy = -1, mm = -1, dd = -1;
|
|
int h, m, s;
|
|
int mday_off, month_off, year_off;
|
|
struct tm tm;
|
|
|
|
/* Skip the '-' before the time */
|
|
mday_off = timeoff;
|
|
if (!mday_off || name[--mday_off] != '-')
|
|
return GIT_WALK_SKIP;
|
|
/* Skip the day name */
|
|
while (mday_off > 0 && name[--mday_off] != '-')
|
|
/* nothing */;
|
|
|
|
mday_off = mday_off - 2;
|
|
month_off = mday_off - 3;
|
|
year_off = month_off - 5;
|
|
if (mday_off < 0)
|
|
return GIT_WALK_SKIP;
|
|
|
|
/* Get the time of day -- parse both time formats so we can read old repos when not on Windows */
|
|
if (sscanf(name + timeoff, "%d:%d:%d", &h, &m, &s) != 3 && sscanf(name + timeoff, "%d=%d=%d", &h, &m, &s) != 3)
|
|
return GIT_WALK_SKIP;
|
|
if (!validate_time(h, m, s))
|
|
return GIT_WALK_SKIP;
|
|
|
|
/*
|
|
* Using the "git_tree_walk()" interface is simple, but
|
|
* it kind of sucks as an interface because there is
|
|
* no sane way to pass the hierarchy to the callbacks.
|
|
* The "payload" is a fixed one-time thing: we'd like
|
|
* the "current trip" to be passed down to the dives
|
|
* that get parsed under that trip, but we can't.
|
|
*
|
|
* So "active_trip" is not the trip that is in the hierarchy
|
|
* _above_ us, it's just the trip that was _before_ us. But
|
|
* if a dive is not in a trip at all, we can't tell.
|
|
*
|
|
* We could just do a better walker that passes the
|
|
* return value around, but we hack around this by
|
|
* instead looking at the one hierarchical piece of
|
|
* data we have: the pathname to the current entry.
|
|
*
|
|
* This is pretty hacky. The magic '8' is the length
|
|
* of a pathname of the form 'yyyy/mm/'.
|
|
*/
|
|
if (strlen(root) == 8)
|
|
finish_active_trip(state);
|
|
|
|
/*
|
|
* Get the date. The day of the month is in the dive directory
|
|
* name, the year and month might be in the path leading up
|
|
* to it.
|
|
*/
|
|
dd = atoi(name + mday_off);
|
|
if (year_off < 0) {
|
|
if (sscanf(root, "%d/%d", &yyyy, &mm) != 2)
|
|
return GIT_WALK_SKIP;
|
|
} else
|
|
yyyy = atoi(name + year_off);
|
|
if (month_off >= 0)
|
|
mm = atoi(name + month_off);
|
|
|
|
if (!validate_date(yyyy, mm, dd))
|
|
return GIT_WALK_SKIP;
|
|
|
|
/* Ok, close enough. We've gotten sufficient information */
|
|
memset(&tm, 0, sizeof(tm));
|
|
tm.tm_hour = h;
|
|
tm.tm_min = m;
|
|
tm.tm_sec = s;
|
|
tm.tm_year = yyyy;
|
|
tm.tm_mon = mm-1;
|
|
tm.tm_mday = dd;
|
|
|
|
finish_active_dive(state);
|
|
create_new_dive(utc_mktime(&tm), state);
|
|
memcpy(state->active_dive->git_id.data(), git_tree_entry_id(entry)->id, 20);
|
|
return GIT_WALK_OK;
|
|
}
|
|
|
|
static int picture_directory(const char *, const char *, struct git_parser_state *state)
|
|
{
|
|
if (!state->active_dive)
|
|
return GIT_WALK_SKIP;
|
|
return GIT_WALK_OK;
|
|
}
|
|
|
|
/*
|
|
* Return the length of the string without the unique part.
|
|
*/
|
|
static int nonunique_length(const char *str)
|
|
{
|
|
int len = 0;
|
|
|
|
for (;;) {
|
|
char c = *str++;
|
|
if (!c || c == '~')
|
|
return len;
|
|
len++;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* When hitting a directory node, we have a couple of cases:
|
|
*
|
|
* - It's just a date entry - all numeric (either year or month):
|
|
*
|
|
* [yyyy|mm]
|
|
*
|
|
* We don't do anything with these, we just traverse into them.
|
|
* The numeric data will show up as part of the full path when
|
|
* we hit more interesting entries.
|
|
*
|
|
* - It's a trip directory. The name will be of the form
|
|
*
|
|
* nn-alphabetic[~hex]
|
|
*
|
|
* where 'nn' is the day of the month (year and month will be
|
|
* encoded in the path leading up to this).
|
|
*
|
|
* - It's a dive directory. The name will be of the form
|
|
*
|
|
* [[yyyy-]mm-]nn-ddd-hh=mm=ss[~hex]
|
|
*
|
|
* (older versions had this as [[yyyy-]mm-]nn-ddd-hh:mm:ss[~hex]
|
|
* but that faile on Windows)
|
|
*
|
|
* which describes the date and time of a dive (yyyy and mm
|
|
* are optional, and may be encoded in the path leading up to
|
|
* the dive).
|
|
*
|
|
* - It is a per-dive picture directory ("Pictures")
|
|
*
|
|
* - It's some random non-dive-data directory.
|
|
*
|
|
* If it doesn't match the above patterns, we'll ignore them
|
|
* for dive loading purposes, and not even recurse into them.
|
|
*/
|
|
static int walk_tree_directory(const char *root, const git_tree_entry *entry, struct git_parser_state *state)
|
|
{
|
|
const char *name = git_tree_entry_name(entry);
|
|
int digits = 0, len;
|
|
char c;
|
|
|
|
if (!strcmp(name, "Pictures"))
|
|
return picture_directory(root, name, state);
|
|
|
|
if (!strcmp(name, "01-Divesites"))
|
|
return GIT_WALK_OK;
|
|
|
|
if (!strcmp(name, "02-Filterpresets"))
|
|
return GIT_WALK_OK;
|
|
|
|
while (isdigit(c = name[digits]))
|
|
digits++;
|
|
|
|
/* Doesn't start with two or four digits? Skip */
|
|
if (digits != 4 && digits != 2)
|
|
return GIT_WALK_SKIP;
|
|
|
|
/* Only digits? Do nothing, but recurse into it */
|
|
if (!c)
|
|
return GIT_WALK_OK;
|
|
|
|
/* All valid cases need to have a slash following */
|
|
if (c != '-')
|
|
return GIT_WALK_SKIP;
|
|
|
|
/* Do a quick check for a common dive case */
|
|
len = nonunique_length(name);
|
|
|
|
/*
|
|
* We know the len is at least 3, because we had at least
|
|
* two digits and a dash
|
|
*/
|
|
if (name[len-3] == ':' || name[len-3] == '=')
|
|
return dive_directory(root, entry, name, len-8, state);
|
|
|
|
if (digits != 2)
|
|
return GIT_WALK_SKIP;
|
|
|
|
return dive_trip_directory(root, name, state);
|
|
}
|
|
|
|
static git_blob *git_tree_entry_blob(git_repository *repo, const git_tree_entry *entry)
|
|
{
|
|
const git_oid *id = git_tree_entry_id(entry);
|
|
git_blob *blob;
|
|
|
|
if (git_blob_lookup(&blob, repo, id))
|
|
return NULL;
|
|
return blob;
|
|
}
|
|
|
|
static struct divecomputer *create_new_dc(struct dive *dive)
|
|
{
|
|
struct divecomputer *dc = &dive->dcs.back();
|
|
|
|
/* Did we already fill that in? */
|
|
if (!dc->samples.empty() || !dc->model.empty() || dc->when) {
|
|
dive->dcs.emplace_back();
|
|
dc = &dive->dcs.back();
|
|
}
|
|
dc->when = dive->when;
|
|
dc->duration = dive->duration;
|
|
return dc;
|
|
}
|
|
|
|
/*
|
|
* We should *really* try to delay the dive computer data parsing
|
|
* until necessary, in order to reduce load-time. The parsing is
|
|
* cheap, but the loading of the git blob into memory can be pretty
|
|
* costly.
|
|
*/
|
|
static int parse_divecomputer_entry(struct git_parser_state *state, const git_tree_entry *entry, const char *)
|
|
{
|
|
git_blob *blob = git_tree_entry_blob(state->repo, entry);
|
|
|
|
if (!blob)
|
|
return report_error("Unable to read divecomputer file");
|
|
|
|
state->active_dc = create_new_dc(state->active_dive.get());
|
|
for_each_line(blob, divecomputer_parser, state);
|
|
git_blob_free(blob);
|
|
state->active_dc = NULL;
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* NOTE! The "git_id" for the dive is the hash for the whole dive directory.
|
|
* As such, it covers not just the dive, but the divecomputers and the
|
|
* pictures too. So if any of the dive computers change, the dive cache
|
|
* has to be invalidated too.
|
|
*/
|
|
static int parse_dive_entry(struct git_parser_state *state, const git_tree_entry *entry, const char *suffix)
|
|
{
|
|
git_blob *blob = git_tree_entry_blob(state->repo, entry);
|
|
if (!blob)
|
|
return report_error("Unable to read dive file");
|
|
if (*suffix)
|
|
state->active_dive->number = atoi(suffix + 1);
|
|
state->active_dive->weightsystems.clear();
|
|
state->o2pressure_sensor = 1;
|
|
for_each_line(blob, dive_parser, state);
|
|
git_blob_free(blob);
|
|
return 0;
|
|
}
|
|
|
|
static int parse_site_entry(struct git_parser_state *state, const git_tree_entry *entry, const char *suffix)
|
|
{
|
|
if (*suffix == '\0')
|
|
return report_error("Dive site without uuid");
|
|
uint32_t uuid = strtoul(suffix, NULL, 16);
|
|
state->active_site = state->log->sites.alloc_or_get(uuid);
|
|
git_blob *blob = git_tree_entry_blob(state->repo, entry);
|
|
if (!blob)
|
|
return report_error("Unable to read dive site file");
|
|
for_each_line(blob, site_parser, state);
|
|
state->active_site = NULL;
|
|
git_blob_free(blob);
|
|
return 0;
|
|
}
|
|
|
|
static int parse_trip_entry(struct git_parser_state *state, const git_tree_entry *entry)
|
|
{
|
|
git_blob *blob = git_tree_entry_blob(state->repo, entry);
|
|
if (!blob)
|
|
return report_error("Unable to read trip file");
|
|
for_each_line(blob, trip_parser, state);
|
|
git_blob_free(blob);
|
|
return 0;
|
|
}
|
|
|
|
static int parse_settings_entry(struct git_parser_state *state, const git_tree_entry *entry)
|
|
{
|
|
git_blob *blob = git_tree_entry_blob(state->repo, entry);
|
|
if (!blob)
|
|
return report_error("Unable to read settings file");
|
|
for_each_line(blob, settings_parser, state);
|
|
git_blob_free(blob);
|
|
return 0;
|
|
}
|
|
|
|
static int parse_picture_entry(struct git_parser_state *state, const git_tree_entry *entry, const char *name)
|
|
{
|
|
git_blob *blob;
|
|
int hh, mm, ss, offset;
|
|
char sign;
|
|
|
|
/*
|
|
* The format of the picture name files is just the offset within
|
|
* the dive in form [[+-]hh=mm=ss (previously [[+-]hh:mm:ss, but
|
|
* that didn't work on Windows), possibly followed by a hash to
|
|
* make the filename unique (which we can just ignore).
|
|
*/
|
|
if (sscanf(name, "%c%d:%d:%d", &sign, &hh, &mm, &ss) != 4 &&
|
|
sscanf(name, "%c%d=%d=%d", &sign, &hh, &mm, &ss) != 4)
|
|
return report_error("Unknown file name %s", name);
|
|
offset = ss + 60 * (mm + 60 * hh);
|
|
if (sign == '-')
|
|
offset = -offset;
|
|
|
|
blob = git_tree_entry_blob(state->repo, entry);
|
|
if (!blob)
|
|
return report_error("Unable to read picture file");
|
|
|
|
state->active_pic.offset.seconds = offset;
|
|
|
|
for_each_line(blob, picture_parser, state);
|
|
add_picture(state->active_dive->pictures, std::move(state->active_pic));
|
|
git_blob_free(blob);
|
|
|
|
/* add_picture took ownership of the data -
|
|
* clear out our copy just to be sure. */
|
|
state->active_pic = picture();
|
|
return 0;
|
|
}
|
|
|
|
static int parse_filter_preset(struct git_parser_state *state, const git_tree_entry *entry)
|
|
{
|
|
git_blob *blob = git_tree_entry_blob(state->repo, entry);
|
|
if (!blob)
|
|
return report_error("Unable to read filter preset file");
|
|
|
|
state->active_filter = std::make_unique<filter_preset>();
|
|
for_each_line(blob, filter_preset_parser, state);
|
|
|
|
git_blob_free(blob);
|
|
|
|
state->log->filter_presets.add(*state->active_filter);
|
|
state->active_filter.reset();
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int walk_tree_file(const char *root, const git_tree_entry *entry, struct git_parser_state *state)
|
|
{
|
|
auto &dive = state->active_dive;
|
|
auto &trip = state->active_trip;
|
|
const char *name = git_tree_entry_name(entry);
|
|
if (verbose > 1)
|
|
report_info("git load handling file %s\n", name);
|
|
switch (*name) {
|
|
case '-': case '+':
|
|
if (dive)
|
|
return parse_picture_entry(state, entry, name);
|
|
break;
|
|
case 'D':
|
|
if (dive && !strncmp(name, "Divecomputer", 12))
|
|
return parse_divecomputer_entry(state, entry, name + 12);
|
|
if (dive && !strncmp(name, "Dive", 4))
|
|
return parse_dive_entry(state, entry, name + 4);
|
|
break;
|
|
case 'P':
|
|
if (!strncmp(name, "Preset-", 7))
|
|
return parse_filter_preset(state, entry);
|
|
break;
|
|
case 'S':
|
|
if (!strncmp(name, "Site", 4))
|
|
return parse_site_entry(state, entry, name + 5);
|
|
break;
|
|
case '0':
|
|
if (trip && !strcmp(name, "00-Trip"))
|
|
return parse_trip_entry(state, entry);
|
|
if (!strcmp(name, "00-Subsurface"))
|
|
return parse_settings_entry(state, entry);
|
|
break;
|
|
}
|
|
report_error("Unknown file %s%s (%p %p)", root, name, dive.get(), trip.get());
|
|
return GIT_WALK_SKIP;
|
|
}
|
|
|
|
static int walk_tree_cb(const char *root, const git_tree_entry *entry, void *payload)
|
|
{
|
|
struct git_parser_state *state = (git_parser_state *)payload;
|
|
git_filemode_t mode = git_tree_entry_filemode(entry);
|
|
|
|
if (mode == GIT_FILEMODE_TREE)
|
|
return walk_tree_directory(root, entry, state);
|
|
|
|
walk_tree_file(root, entry, state);
|
|
/* Ignore failed blob loads */
|
|
return GIT_WALK_OK;
|
|
}
|
|
|
|
static int load_dives_from_tree(git_repository *repo, git_tree *tree, struct git_parser_state *state)
|
|
{
|
|
git_tree_walk(tree, GIT_TREEWALK_PRE, walk_tree_cb, state);
|
|
return 0;
|
|
}
|
|
|
|
void clear_git_id()
|
|
{
|
|
saved_git_id.clear();
|
|
}
|
|
|
|
void set_git_id(const struct git_oid *id)
|
|
{
|
|
char git_id_buffer[GIT_OID_HEXSZ + 1];
|
|
|
|
git_oid_tostr(git_id_buffer, sizeof(git_id_buffer), id);
|
|
saved_git_id = git_id_buffer;
|
|
}
|
|
|
|
static int find_commit(git_repository *repo, const char *branch, git_commit **commit_p)
|
|
{
|
|
git_object *object;
|
|
|
|
if (git_revparse_single(&object, repo, branch))
|
|
return report_error("Unable to look up revision '%s'", branch);
|
|
if (git_object_peel((git_object **)commit_p, object, GIT_OBJ_COMMIT))
|
|
return report_error("Revision '%s' is not a valid commit", branch);
|
|
return 0;
|
|
}
|
|
|
|
static int do_git_load(git_repository *repo, const char *branch, struct git_parser_state *state)
|
|
{
|
|
int ret;
|
|
git_commit *commit;
|
|
git_tree *tree;
|
|
|
|
ret = find_commit(repo, branch, &commit);
|
|
if (ret)
|
|
return ret;
|
|
if (git_commit_tree(&tree, commit))
|
|
return report_error("Could not look up tree of commit in branch '%s'", branch);
|
|
git_storage_update_progress(translate("gettextFromC", "Load dives from local cache"));
|
|
ret = load_dives_from_tree(repo, tree, state);
|
|
if (!ret) {
|
|
set_git_id(git_commit_id(commit));
|
|
git_storage_update_progress(translate("gettextFromC", "Successfully opened dive data"));
|
|
}
|
|
git_object_free((git_object *)tree);
|
|
|
|
return ret;
|
|
}
|
|
|
|
std::string get_sha(git_repository *repo, const std::string &branch)
|
|
{
|
|
char git_id_buffer[GIT_OID_HEXSZ + 1];
|
|
git_commit *commit;
|
|
if (find_commit(repo, branch.c_str(), &commit))
|
|
return std::string();
|
|
git_oid_tostr(git_id_buffer, sizeof(git_id_buffer), (const git_oid *)commit);
|
|
return std::string(git_id_buffer);
|
|
}
|
|
|
|
/*
|
|
* Like git_save_dives(), this silently returns a negative
|
|
* value if it's not a git repository at all (so that you
|
|
* can try to load it some other way.
|
|
*
|
|
* If it is a git repository, we return zero for success,
|
|
* or report an error and return 1 if the load failed.
|
|
*/
|
|
int git_load_dives(struct git_info *info, struct divelog *log)
|
|
{
|
|
int ret;
|
|
struct git_parser_state state;
|
|
state.repo = info->repo;
|
|
state.log = log;
|
|
|
|
if (!info->repo)
|
|
return report_error("Unable to open git repository '%s[%s]'", info->url.c_str(), info->branch.c_str());
|
|
ret = do_git_load(info->repo, info->branch.c_str(), &state);
|
|
finish_active_dive(&state);
|
|
finish_active_trip(&state);
|
|
return ret;
|
|
}
|