diff -Nru flightgear-2017.3.1+dfsg/3rdparty/CMakeLists.txt flightgear-2018.1.1+dfsg/3rdparty/CMakeLists.txt --- flightgear-2017.3.1+dfsg/3rdparty/CMakeLists.txt 2017-09-20 07:51:30.000000000 +0000 +++ flightgear-2018.1.1+dfsg/3rdparty/CMakeLists.txt 2018-04-06 19:43:33.000000000 +0000 @@ -25,3 +25,7 @@ include_directories(${PROJECT_SOURCE_DIR}/3rdparty/hts_engine_API/include ) endif() endif() + +if (ENABLE_HID_INPUT) + add_subdirectory(hidapi) +endif() diff -Nru flightgear-2017.3.1+dfsg/3rdparty/hidapi/CMakeLists.txt flightgear-2018.1.1+dfsg/3rdparty/hidapi/CMakeLists.txt --- flightgear-2017.3.1+dfsg/3rdparty/hidapi/CMakeLists.txt 1970-01-01 00:00:00.000000000 +0000 +++ flightgear-2018.1.1+dfsg/3rdparty/hidapi/CMakeLists.txt 2018-04-06 19:43:33.000000000 +0000 @@ -0,0 +1,30 @@ + +set(HIDAPI_SOURCES + hidapi/hidapi.h + hidapi/hidparse.h + hidparser/hidparse.c +) + +if (WIN32) + list(APPEND HIDAPI_SOURCES windows/hid.c) +elseif(APPLE) + list(APPEND HIDAPI_SOURCES mac/hid.c) +else() + list(APPEND HIDAPI_SOURCES linux/hid.c) +endif(WIN32) + + +#add_definitions( -DHAVE_CONFIG_H ) # to use fgfs config.h to get FG version, if needed +#add_definitions( -DLIBVER="SVN 261" ) # add an iaxclient_lib version string + +add_library(hidapi STATIC + ${HIDAPI_SOURCES} +) + +target_link_libraries(hidapi ${UDEV_LIBRARIES}) + + +target_include_directories(hidapi PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/hidapi) +target_include_directories(hidapi PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) + +# eof diff -Nru flightgear-2017.3.1+dfsg/3rdparty/hidapi/hidapi/hidapi.h flightgear-2018.1.1+dfsg/3rdparty/hidapi/hidapi/hidapi.h --- flightgear-2017.3.1+dfsg/3rdparty/hidapi/hidapi/hidapi.h 1970-01-01 00:00:00.000000000 +0000 +++ flightgear-2018.1.1+dfsg/3rdparty/hidapi/hidapi/hidapi.h 2018-04-06 19:43:33.000000000 +0000 @@ -0,0 +1,403 @@ +/******************************************************* + HIDAPI - Multi-Platform library for + communication with HID devices. + + Alan Ott + Signal 11 Software + + 8/22/2009 + + Copyright 2009, All Rights Reserved. + + At the discretion of the user of this library, + this software may be licensed under the terms of the + GNU General Public License v3, a BSD-Style license, or the + original HIDAPI license as outlined in the LICENSE.txt, + LICENSE-gpl3.txt, LICENSE-bsd.txt, and LICENSE-orig.txt + files located at the root of the source distribution. + These files may also be found in the public source + code repository located at: + http://github.com/signal11/hidapi . +********************************************************/ + +/** @file + * @defgroup API hidapi API + */ + +#ifndef HIDAPI_H__ +#define HIDAPI_H__ + +#include + +#ifdef _WIN32 + #define HID_API_EXPORT __declspec(dllexport) + #define HID_API_CALL +#else + #define HID_API_EXPORT /**< API export macro */ + #define HID_API_CALL /**< API call macro */ +#endif + +#define HID_API_EXPORT_CALL HID_API_EXPORT HID_API_CALL /**< API export and call macro*/ + +#ifdef __cplusplus +extern "C" { +#endif + struct hid_device_; + typedef struct hid_device_ hid_device; /**< opaque hidapi structure */ + + /** hidapi info structure */ + struct hid_device_info { + /** Platform-specific device path */ + char *path; + /** Device Vendor ID */ + unsigned short vendor_id; + /** Device Product ID */ + unsigned short product_id; + /** Serial Number */ + wchar_t *serial_number; + /** Device Release Number in binary-coded decimal, + also known as Device Version Number */ + unsigned short release_number; + /** Manufacturer String */ + wchar_t *manufacturer_string; + /** Product string */ + wchar_t *product_string; + /** Usage Page for this Device/Interface + (Windows/Mac only). */ + unsigned short usage_page; + /** Usage for this Device/Interface + (Windows/Mac only).*/ + unsigned short usage; + /** The USB interface which this logical device + represents. Valid on both Linux implementations + in all cases, and valid on the Windows implementation + only if the device contains more than one interface. */ + int interface_number; + + /** Pointer to the next device */ + struct hid_device_info *next; + }; + + + /** @brief Initialize the HIDAPI library. + + This function initializes the HIDAPI library. Calling it is not + strictly necessary, as it will be called automatically by + hid_enumerate() and any of the hid_open_*() functions if it is + needed. This function should be called at the beginning of + execution however, if there is a chance of HIDAPI handles + being opened by different threads simultaneously. + + @ingroup API + + @returns + This function returns 0 on success and -1 on error. + */ + int HID_API_EXPORT HID_API_CALL hid_init(void); + + /** @brief Finalize the HIDAPI library. + + This function frees all of the static data associated with + HIDAPI. It should be called at the end of execution to avoid + memory leaks. + + @ingroup API + + @returns + This function returns 0 on success and -1 on error. + */ + int HID_API_EXPORT HID_API_CALL hid_exit(void); + + /** @brief Enumerate the HID Devices. + + This function returns a linked list of all the HID devices + attached to the system which match vendor_id and product_id. + If @p vendor_id is set to 0 then any vendor matches. + If @p product_id is set to 0 then any product matches. + If @p vendor_id and @p product_id are both set to 0, then + all HID devices will be returned. + + @ingroup API + @param vendor_id The Vendor ID (VID) of the types of device + to open. + @param product_id The Product ID (PID) of the types of + device to open. + + @returns + This function returns a pointer to a linked list of type + struct #hid_device, containing information about the HID devices + attached to the system, or NULL in the case of failure. Free + this linked list by calling hid_free_enumeration(). + */ + struct hid_device_info HID_API_EXPORT * HID_API_CALL hid_enumerate(unsigned short vendor_id, unsigned short product_id); + + /** @brief Free an enumeration Linked List + + This function frees a linked list created by hid_enumerate(). + + @ingroup API + @param devs Pointer to a list of struct_device returned from + hid_enumerate(). + */ + void HID_API_EXPORT HID_API_CALL hid_free_enumeration(struct hid_device_info *devs); + + /** @brief Open a HID device using a Vendor ID (VID), Product ID + (PID) and optionally a serial number. + + If @p serial_number is NULL, the first device with the + specified VID and PID is opened. + + @ingroup API + @param vendor_id The Vendor ID (VID) of the device to open. + @param product_id The Product ID (PID) of the device to open. + @param serial_number The Serial Number of the device to open + (Optionally NULL). + + @returns + This function returns a pointer to a #hid_device object on + success or NULL on failure. + */ + HID_API_EXPORT hid_device * HID_API_CALL hid_open(unsigned short vendor_id, unsigned short product_id, const wchar_t *serial_number); + + /** @brief Open a HID device by its path name. + + The path name be determined by calling hid_enumerate(), or a + platform-specific path name can be used (eg: /dev/hidraw0 on + Linux). + + @ingroup API + @param path The path name of the device to open + + @returns + This function returns a pointer to a #hid_device object on + success or NULL on failure. + */ + HID_API_EXPORT hid_device * HID_API_CALL hid_open_path(const char *path); + + /** @brief Write an Output report to a HID device. + + The first byte of @p data[] must contain the Report ID. For + devices which only support a single report, this must be set + to 0x0. The remaining bytes contain the report data. Since + the Report ID is mandatory, calls to hid_write() will always + contain one more byte than the report contains. For example, + if a hid report is 16 bytes long, 17 bytes must be passed to + hid_write(), the Report ID (or 0x0, for devices with a + single report), followed by the report data (16 bytes). In + this example, the length passed in would be 17. + + hid_write() will send the data on the first OUT endpoint, if + one exists. If it does not, it will send the data through + the Control Endpoint (Endpoint 0). + + @ingroup API + @param device A device handle returned from hid_open(). + @param data The data to send, including the report number as + the first byte. + @param length The length in bytes of the data to send. + + @returns + This function returns the actual number of bytes written and + -1 on error. + */ + int HID_API_EXPORT HID_API_CALL hid_write(hid_device *device, const unsigned char *data, size_t length); + + /** @brief Read an Input report from a HID device with timeout. + + Input reports are returned + to the host through the INTERRUPT IN endpoint. The first byte will + contain the Report number if the device uses numbered reports. + + @ingroup API + @param device A device handle returned from hid_open(). + @param data A buffer to put the read data into. + @param length The number of bytes to read. For devices with + multiple reports, make sure to read an extra byte for + the report number. + @param milliseconds timeout in milliseconds or -1 for blocking wait. + + @returns + This function returns the actual number of bytes read and + -1 on error. If no packet was available to be read within + the timeout period, this function returns 0. + */ + int HID_API_EXPORT HID_API_CALL hid_read_timeout(hid_device *dev, unsigned char *data, size_t length, int milliseconds); + + /** @brief Read an Input report from a HID device. + + Input reports are returned + to the host through the INTERRUPT IN endpoint. The first byte will + contain the Report number if the device uses numbered reports. + + @ingroup API + @param device A device handle returned from hid_open(). + @param data A buffer to put the read data into. + @param length The number of bytes to read. For devices with + multiple reports, make sure to read an extra byte for + the report number. + + @returns + This function returns the actual number of bytes read and + -1 on error. If no packet was available to be read and + the handle is in non-blocking mode, this function returns 0. + */ + int HID_API_EXPORT HID_API_CALL hid_read(hid_device *device, unsigned char *data, size_t length); + + /** @brief Set the device handle to be non-blocking. + + In non-blocking mode calls to hid_read() will return + immediately with a value of 0 if there is no data to be + read. In blocking mode, hid_read() will wait (block) until + there is data to read before returning. + + Nonblocking can be turned on and off at any time. + + @ingroup API + @param device A device handle returned from hid_open(). + @param nonblock enable or not the nonblocking reads + - 1 to enable nonblocking + - 0 to disable nonblocking. + + @returns + This function returns 0 on success and -1 on error. + */ + int HID_API_EXPORT HID_API_CALL hid_set_nonblocking(hid_device *device, int nonblock); + + /** @brief Send a Feature report to the device. + + Feature reports are sent over the Control endpoint as a + Set_Report transfer. The first byte of @p data[] must + contain the Report ID. For devices which only support a + single report, this must be set to 0x0. The remaining bytes + contain the report data. Since the Report ID is mandatory, + calls to hid_send_feature_report() will always contain one + more byte than the report contains. For example, if a hid + report is 16 bytes long, 17 bytes must be passed to + hid_send_feature_report(): the Report ID (or 0x0, for + devices which do not use numbered reports), followed by the + report data (16 bytes). In this example, the length passed + in would be 17. + + @ingroup API + @param device A device handle returned from hid_open(). + @param data The data to send, including the report number as + the first byte. + @param length The length in bytes of the data to send, including + the report number. + + @returns + This function returns the actual number of bytes written and + -1 on error. + */ + int HID_API_EXPORT HID_API_CALL hid_send_feature_report(hid_device *device, const unsigned char *data, size_t length); + + /** @brief Get a feature report from a HID device. + + Set the first byte of @p data[] to the Report ID of the + report to be read. Make sure to allow space for this + extra byte in @p data[]. Upon return, the first byte will + still contain the Report ID, and the report data will + start in data[1]. + + @ingroup API + @param device A device handle returned from hid_open(). + @param data A buffer to put the read data into, including + the Report ID. Set the first byte of @p data[] to the + Report ID of the report to be read, or set it to zero + if your device does not use numbered reports. + @param length The number of bytes to read, including an + extra byte for the report ID. The buffer can be longer + than the actual report. + + @returns + This function returns the number of bytes read plus + one for the report ID (which is still in the first + byte), or -1 on error. + */ + int HID_API_EXPORT HID_API_CALL hid_get_feature_report(hid_device *device, unsigned char *data, size_t length); + + /** @brief Close a HID device. + + @ingroup API + @param device A device handle returned from hid_open(). + */ + void HID_API_EXPORT HID_API_CALL hid_close(hid_device *device); + + /** @brief Get The Manufacturer String from a HID device. + + @ingroup API + @param device A device handle returned from hid_open(). + @param string A wide string buffer to put the data into. + @param maxlen The length of the buffer in multiples of wchar_t. + + @returns + This function returns 0 on success and -1 on error. + */ + int HID_API_EXPORT_CALL hid_get_manufacturer_string(hid_device *device, wchar_t *string, size_t maxlen); + + /** @brief Get The Product String from a HID device. + + @ingroup API + @param device A device handle returned from hid_open(). + @param string A wide string buffer to put the data into. + @param maxlen The length of the buffer in multiples of wchar_t. + + @returns + This function returns 0 on success and -1 on error. + */ + int HID_API_EXPORT_CALL hid_get_product_string(hid_device *device, wchar_t *string, size_t maxlen); + + /** @brief Get The Serial Number String from a HID device. + + @ingroup API + @param device A device handle returned from hid_open(). + @param string A wide string buffer to put the data into. + @param maxlen The length of the buffer in multiples of wchar_t. + + @returns + This function returns 0 on success and -1 on error. + */ + int HID_API_EXPORT_CALL hid_get_serial_number_string(hid_device *device, wchar_t *string, size_t maxlen); + + /** @brief Get a string from a HID device, based on its string index. + + @ingroup API + @param device A device handle returned from hid_open(). + @param string_index The index of the string to get. + @param string A wide string buffer to put the data into. + @param maxlen The length of the buffer in multiples of wchar_t. + + @returns + This function returns 0 on success and -1 on error. + */ + int HID_API_EXPORT_CALL hid_get_indexed_string(hid_device *device, int string_index, wchar_t *string, size_t maxlen); + + /** @brief Get a string describing the last error which occurred. + + @ingroup API + @param device A device handle returned from hid_open(). + + @returns + This function returns a string containing the last error + which occurred or NULL if none has occurred. + */ + HID_API_EXPORT const wchar_t* HID_API_CALL hid_error(hid_device *device); + + /** @brief Get the HID descriptor from the device + + @ingroup API + @param device A device handle returned from hid_open(). + @param data A buffer to put the descriptor data into. + @param length The size of the buffer + + @returns + This function returns the actual number of bytes read and + -1 on error. + */ + int HID_API_EXPORT HID_API_CALL hid_get_descriptor(hid_device *device, unsigned char *data, size_t length); +#ifdef __cplusplus +} +#endif + +#endif + diff -Nru flightgear-2017.3.1+dfsg/3rdparty/hidapi/hidapi/hidparse.h flightgear-2018.1.1+dfsg/3rdparty/hidapi/hidapi/hidparse.h --- flightgear-2017.3.1+dfsg/3rdparty/hidapi/hidapi/hidparse.h 1970-01-01 00:00:00.000000000 +0000 +++ flightgear-2018.1.1+dfsg/3rdparty/hidapi/hidapi/hidparse.h 2018-04-06 19:43:33.000000000 +0000 @@ -0,0 +1,51 @@ + +#ifndef HIDPARSE_H__ +#define HIDPARSE_H__ + +#include + +#include "hidapi.h" + +#ifdef __cplusplus +extern "C" { +#endif + + typedef struct hid_item_s { + + /** + * @brief next our sibling items + */ + struct hid_item_s *next; + + /** + * @brief collection our child items + */ + struct hid_item_s *collection; + + /** + * @brief parent our parent collection + */ + struct hid_item_s *parent; + + uint32_t usage; + uint8_t type; + + uint8_t report_size; /* in bits */ + uint8_t report_id; + uint8_t flags; + + int32_t logical_min, logical_max, physical_min, physical_max; + uint32_t unit, unit_exponent; + } hid_item; + +int HID_API_EXPORT HID_API_CALL hid_parse_reportdesc(uint8_t* rdesc_buf, uint32_t rdesc_size, hid_item** root); + +void HID_API_EXPORT HID_API_CALL hid_free_reportdesc(hid_item* root); + +int HID_API_EXPORT HID_API_CALL hid_parse_is_relative(hid_item* item); + +#ifdef __cplusplus +} +#endif + +#endif diff -Nru flightgear-2017.3.1+dfsg/3rdparty/hidapi/hidparser/hidparse.c flightgear-2018.1.1+dfsg/3rdparty/hidapi/hidparser/hidparse.c --- flightgear-2017.3.1+dfsg/3rdparty/hidapi/hidparser/hidparse.c 1970-01-01 00:00:00.000000000 +0000 +++ flightgear-2018.1.1+dfsg/3rdparty/hidapi/hidparser/hidparse.c 2018-04-06 19:43:33.000000000 +0000 @@ -0,0 +1,428 @@ +#include "hidparse.h" + +#include +#include +#include +#include + +static uint8_t size_to_bytes(uint8_t sz) +{ + switch (sz) + { + case 0: return 0; + case 1: return 1; + case 2: return 2; + case 3: return 4; + default: return 0; + } +} + +static int32_t read_int(uint8_t sz, uint8_t* raw) +{ + switch (sz) + { + case 1: return *((int8_t*) raw); // ensure we cast to sign-extend + case 2: return *((int16_t*) raw); + case 3: return *((int32_t*) raw); + } + + assert(0); + return 0; +} + +static uint32_t read_uint(uint8_t sz, uint8_t* raw) +{ + switch (sz) + { + case 1: return *raw; + case 2: return *((uint16_t*) raw); + case 3: return *((uint32_t*) raw); + } + + assert(0); + return 0; +} + + +const uint8_t HID_LONG_ITEM = 0xFE; // see 6.2.2.3 + +// following definitions come from 6.2.2.4 +#define HID_COLLECTION_ITEM 0xA +#define HID_END_COLLECTION_ITEM 0xC +#define HID_INPUT_ITEM 0x8 +#define HID_OUTPUT_ITEM 0x9 +#define HID_FEATURE_ITEM 0xB + +#define HID_USAGE_PAGE_ITEM 0x0 +#define HID_LOGICAL_MINIMUM_ITEM 0x1 +#define HID_LOGICAL_MAXIMUM_ITEM 0x2 +#define HID_PHYSICAL_MINIMUM_ITEM 0x3 +#define HID_PHYSICAL_MAXIMUM_ITEM 0x4 +#define HID_EXPONENT_ITEM 0x5 +#define HID_UNIT_ITEM 0x6 +#define HID_REPORT_SIZE_ITEM 0x7 +#define HID_REPORT_ID_ITEM 0x8 +#define HID_REPORT_COUNT_ITEM 0x9 +#define HID_PUSH_ITEM 0xA +#define HID_POP_ITEM 0xB + +// following definitions come from 6.2.2.8 +#define HID_USAGE_ITEM 0x0 +#define HID_USAGE_MINIMUM_ITEM 0x1 +#define HID_USAGE_MAXIMUM_ITEM 0x2 +#define HID_DESIGNATOR_INDEX_ITEM 0x3 +#define HID_DESIGNATOR_MINIMUM_ITEM 0x4 +#define HID_DESIGNATOR_MAXIMUM_ITEM 0x5 +#define HID_STRING_INDEX_ITEM 0x7 +#define HID_STRING_MINIMUM_ITEM 0x8 +#define HID_STRING_MAXIMUM_ITEM 0x9 +#define HID_DELIMITER_ITEM 0xA + +#define MAX_ITEM_STACK 4 + +hid_item item_stack[MAX_ITEM_STACK]; +uint8_t item_stack_size = 0; + +uint32_t usage_array[16]; +uint8_t usage_count; + +void init_local_data(); +void clear_local_data(); + +void set_local_minimum(uint8_t tag, uint32_t data); +void set_local_maximum(uint8_t tag, uint32_t data); + +void push_local_item(uint8_t tag, uint32_t data); + +uint32_t pop_local_item(uint8_t tag); + +hid_item* build_item(uint8_t tag, uint8_t flags, hid_item* global_state) +{ + hid_item* item = (hid_item*) calloc(sizeof(hid_item), 1); + item->flags = flags; + item->usage = pop_local_item(HID_USAGE_ITEM); + item->type = tag; + +// copy from global state + item->report_size = global_state->report_size; + item->logical_min = global_state->logical_min; + item->logical_max = global_state->logical_max; + item->physical_min = global_state->physical_min; + item->physical_max = global_state->physical_max; + item->unit = global_state->unit; + item->unit_exponent = global_state->unit_exponent; + item->report_id = global_state->report_id; + + return item; +} + +hid_item* build_collection(uint8_t collectionType) +{ + hid_item* col = (hid_item*) calloc(sizeof(hid_item), 1); + assert(col); + col->type = collectionType; + col->usage = pop_local_item(HID_USAGE_ITEM); + return col; +} + +void append_item_with_head(hid_item* head, hid_item* i) +{ + assert(head); + for (; head->next; head = head->next) {} + head->next = i; +} + +void append_item(hid_item* col, hid_item* i) +{ + assert(col); + assert(i); + assert(i->next == NULL); + assert(i->parent == NULL); + + i->parent = col; + hid_item* last = col->collection; + if (!last) + { + col->collection = i; + return; + } + + append_item_with_head(last, i); +} + +int hid_parse_reportdesc(uint8_t* rdesc_buf, uint32_t rdesc_size, hid_item** item) +{ + hid_item current_state; + uint8_t report_id = 0; + uint8_t report_count = 1; + uint8_t *p = rdesc_buf; + uint8_t* pEnd = p + rdesc_size; + + hid_item* root_collection = NULL; + hid_item* current_collection = NULL; + + // zero the entire array + memset(item_stack, 0, sizeof(hid_item) * MAX_ITEM_STACK); + item_stack_size = 0; + + memset(¤t_state, 0, sizeof(hid_item)); + usage_count = 0; + + init_local_data(); + + while (p < pEnd) + { + /* See 6.2.2.2 Short Items */ + uint8_t pfx = *p++; + if (pfx == HID_LONG_ITEM) + { + printf("encountered long item\n"); + } + + uint8_t size = pfx & 0x3; /* bits 0-1 */ + uint8_t bytes = size_to_bytes(size); + uint8_t type = (pfx >> 2) & 0x3; /* bits 3-2 */ + uint8_t tag = pfx >> 4; + + // fprintf(stderr, "short item: size=%d, bytes=%d, type = %d, tag=%d\n", + // size, bytes, type, tag); + + /* If it's a main item */ + if (type == 0) + { + switch (tag) + { + case HID_COLLECTION_ITEM: + { + //fprintf(stderr, "opening collection\n"); + uint8_t flags = read_uint(size, p); + hid_item* col = build_collection(flags); + if (!current_collection) { + if (root_collection) { + append_item_with_head(root_collection, col); + } else { + root_collection = col; + } + current_collection = col; + } else { + append_item(current_collection, col); + current_collection = col; + } + break; + } + + case HID_END_COLLECTION_ITEM: + //fprintf(stderr, "closing collection\n"); + assert(current_collection); + current_collection = current_collection->parent; + break; + + case HID_INPUT_ITEM: + case HID_OUTPUT_ITEM: + case HID_FEATURE_ITEM: + { + int i; + uint8_t flags = read_uint(size, p); + + for (i=0; i 0); + current_state = item_stack[--item_stack_size]; + break; + + case HID_PUSH_ITEM: + item_stack[item_stack_size++] = current_state; + break; + } + } else if (type == 2) { + /* it's a local item */ + uint32_t value = read_uint(size, p); + switch (tag) + { + /* section 6.2.2.8, remark about Usage size; current_state only + ever holds a usage page + */ + case HID_USAGE_ITEM: + if (size < sizeof(uint32_t)) + value |= current_state.usage; + push_local_item(tag, value); + break; + + case HID_USAGE_MINIMUM_ITEM: + set_local_minimum(HID_USAGE_ITEM, value | current_state.usage); + break; + + case HID_USAGE_MAXIMUM_ITEM: + set_local_maximum(HID_USAGE_ITEM, value | current_state.usage); + break; + + case HID_DESIGNATOR_MINIMUM_ITEM: + set_local_minimum(HID_DESIGNATOR_INDEX_ITEM, value); + break; + + case HID_DESIGNATOR_MAXIMUM_ITEM: + set_local_maximum(HID_DESIGNATOR_INDEX_ITEM, value); + break; + + case HID_STRING_MINIMUM_ITEM: + set_local_minimum(HID_STRING_INDEX_ITEM, value); + break; + + case HID_STRING_MAXIMUM_ITEM: + set_local_maximum(HID_STRING_INDEX_ITEM, value); + break; + + default: + push_local_item(tag, value); + } + } else { + fprintf(stderr, "bad type value: %d\n", type); + } + + p += bytes; + } + + clear_local_data(); // free transient parsing data + *item = root_collection; + return 0; +} + +void hid_free_reportdesc(hid_item* root) +{ + free(root); +} + +int hid_parse_is_relative(hid_item* item) +{ + return item->flags & 0x04; // bit 2 according to HID 6.2.2.5 +} + + +typedef struct { + uint8_t allocated, count; + uint32_t* d; + uint32_t minimum, maximum, step; +} local_data_array; + +local_data_array local_data_store[0xf]; + +void init_local_data() +{ + int i; + for (i=0; i<0xf; ++i) { + memset(&local_data_store[i], 0, sizeof(local_data_array)); + } +} + +void clear_local_data() +{ + int i; + for (i=0; i<0xf; ++i) { + if (local_data_store[i].d != NULL) { + free(local_data_store[i].d); + } + + memset(&local_data_store[i], 0, sizeof(local_data_array)); + } +} + +void reallocate_local_item(uint8_t tag) +{ + local_data_array* arr = &local_data_store[tag >> 4]; + arr->allocated = (arr->allocated == 0) ? 4 : arr->allocated << 1; + arr->d = realloc(arr->d, sizeof(uint32_t) * arr->allocated); +} + +void push_local_item(uint8_t tag, uint32_t data) +{ + local_data_array* arr = &local_data_store[tag >> 4]; + if (arr->allocated == arr->count) + reallocate_local_item(tag); + + arr->d[arr->count++] = data; +} + +uint32_t pop_local_item(uint8_t tag) +{ + local_data_array* arr = &local_data_store[tag >> 4]; + if (arr->minimum) + { + uint32_t result = arr->minimum + arr->step++; + if (arr->maximum && (result > arr->maximum)) { + fprintf(stderr, "hidparse.c:pop_local_item: range exceeded for tag %d", tag); + } + + return result; + } + + if (arr->count == 0) + return 0; + + uint32_t result = arr->d[arr->count - 1]; + if (arr->count > 1) + --arr->count; /* pop off the back */ + return result; +} + +void set_local_minimum(uint8_t tag, uint32_t data) +{ + local_data_array* arr = &local_data_store[tag >> 4]; + arr->minimum = data; +} + +void set_local_maximum(uint8_t tag, uint32_t data) +{ + local_data_array* arr = &local_data_store[tag >> 4]; + arr->maximum = data; +} diff -Nru flightgear-2017.3.1+dfsg/3rdparty/hidapi/hidparser/testparse.c flightgear-2018.1.1+dfsg/3rdparty/hidapi/hidparser/testparse.c --- flightgear-2017.3.1+dfsg/3rdparty/hidapi/hidparser/testparse.c 1970-01-01 00:00:00.000000000 +0000 +++ flightgear-2018.1.1+dfsg/3rdparty/hidapi/hidparser/testparse.c 2018-04-06 19:43:33.000000000 +0000 @@ -0,0 +1,88 @@ +#include "hidapi.h" +#include "hidparse.h" + +#include +#include +#include + +const char padding[] = " "; // sixteen spaces + + + + +void print_report_descriptor(FILE* fp, int depth, hid_item* item) +{ + if (item->collection) { + hid_item* c = item->collection; + for (; c != NULL; c = c->next) { + print_report_descriptor(fp, depth + 1, c); + } + } else { + // it's a leaf + fprintf(fp, "%sitem type %02x usage %08x bits %2d, report %02x\n", padding + (16 - depth), + item->type, item->usage, item->report_size, item->report_id); + } +} + +int main(int argc, char* argv[]) +{ + #define MAX_STR 255 + wchar_t wstr[MAX_STR]; + int res; + + if (hid_init()) + return EXIT_FAILURE; + + if (argc == 1) { + struct hid_device_info *devs, *cur_dev; + devs = hid_enumerate(0x0, 0x0); + cur_dev = devs; + while (cur_dev) { + printf("Device Found\n type: %04hx %04hx\n path: %s\n serial_number: %ls", cur_dev->vendor_id, cur_dev->product_id, cur_dev->path, cur_dev->serial_number); + printf("\n"); + printf(" Manufacturer: %ls\n", cur_dev->manufacturer_string); + printf(" Product: %ls\n", cur_dev->product_string); + printf(" Release: %hx\n", cur_dev->release_number); + printf(" Interface: %d\n", cur_dev->interface_number); + printf("\n"); + cur_dev = cur_dev->next; + } + hid_free_enumeration(devs); + return EXIT_SUCCESS; + } + + // assume argv[1] is path + hid_device *handle; + handle = hid_open_path(argv[1]); + if (!handle) { + fprintf(stderr, "unable to open device with path %s\n", argv[1]); + return EXIT_FAILURE; + } + + wstr[0] = 0x0000; + res = hid_get_product_string(handle, wstr, MAX_STR); + if (res < 0) + fprintf(stderr, "Unable to read product string\n"); + printf("Product String: %ls\n", wstr); + + unsigned char descriptorBuffer[8192]; + int descriptorBytes = hid_get_descriptor(handle, descriptorBuffer, sizeof(descriptorBuffer)); + if (descriptorBytes < 0) { + fprintf(stderr, "failed to read report descriptor"); + return EXIT_FAILURE; + } + + hid_item* rootItem; + res = hid_parse_reportdesc(descriptorBuffer, descriptorBytes, &rootItem); + if (res != 0) { + fprintf(stderr, "failure parsing report"); + return EXIT_FAILURE; + } + + print_report_descriptor(stdout, 0, rootItem); + + + hid_free_reportdesc(rootItem); + + return EXIT_SUCCESS; +} \ No newline at end of file diff -Nru flightgear-2017.3.1+dfsg/3rdparty/hidapi/linux/hid.c flightgear-2018.1.1+dfsg/3rdparty/hidapi/linux/hid.c --- flightgear-2017.3.1+dfsg/3rdparty/hidapi/linux/hid.c 1970-01-01 00:00:00.000000000 +0000 +++ flightgear-2018.1.1+dfsg/3rdparty/hidapi/linux/hid.c 2018-04-06 19:43:33.000000000 +0000 @@ -0,0 +1,833 @@ +/******************************************************* + HIDAPI - Multi-Platform library for + communication with HID devices. + + Alan Ott + Signal 11 Software + + 8/22/2009 + Linux Version - 6/2/2009 + + Copyright 2009, All Rights Reserved. + + At the discretion of the user of this library, + this software may be licensed under the terms of the + GNU General Public License v3, a BSD-Style license, or the + original HIDAPI license as outlined in the LICENSE.txt, + LICENSE-gpl3.txt, LICENSE-bsd.txt, and LICENSE-orig.txt + files located at the root of the source distribution. + These files may also be found in the public source + code repository located at: + http://github.com/signal11/hidapi . +********************************************************/ + +/* C */ +#include +#include +#include +#include +#include + +/* Unix */ +#include +#include +#include +#include +#include +#include +#include + +/* Linux */ +#include +#include +#include +#include + +#include "hidapi.h" + +/* Definitions from linux/hidraw.h. Since these are new, some distros + may not have header files which contain them. */ +#ifndef HIDIOCSFEATURE +#define HIDIOCSFEATURE(len) _IOC(_IOC_WRITE|_IOC_READ, 'H', 0x06, len) +#endif +#ifndef HIDIOCGFEATURE +#define HIDIOCGFEATURE(len) _IOC(_IOC_WRITE|_IOC_READ, 'H', 0x07, len) +#endif + + +/* USB HID device property names */ +const char *device_string_names[] = { + "manufacturer", + "product", + "serial", +}; + +/* Symbolic names for the properties above */ +enum device_string_id { + DEVICE_STRING_MANUFACTURER, + DEVICE_STRING_PRODUCT, + DEVICE_STRING_SERIAL, + + DEVICE_STRING_COUNT, +}; + +struct hid_device_ { + int device_handle; + int blocking; + int uses_numbered_reports; +}; + + +static __u32 kernel_version = 0; + +static __u32 detect_kernel_version(void) +{ + struct utsname name; + int major, minor, release; + int ret; + + uname(&name); + ret = sscanf(name.release, "%d.%d.%d", &major, &minor, &release); + if (ret == 3) { + return KERNEL_VERSION(major, minor, release); + } + + ret = sscanf(name.release, "%d.%d", &major, &minor); + if (ret == 2) { + return KERNEL_VERSION(major, minor, 0); + } + + printf("Couldn't determine kernel version from version string \"%s\"\n", name.release); + return 0; +} + +static hid_device *new_hid_device(void) +{ + hid_device *dev = calloc(1, sizeof(hid_device)); + dev->device_handle = -1; + dev->blocking = 1; + dev->uses_numbered_reports = 0; + + return dev; +} + + +/* The caller must free the returned string with free(). */ +static wchar_t *utf8_to_wchar_t(const char *utf8) +{ + wchar_t *ret = NULL; + + if (utf8) { + size_t wlen = mbstowcs(NULL, utf8, 0); + if ((size_t) -1 == wlen) { + return wcsdup(L""); + } + ret = calloc(wlen+1, sizeof(wchar_t)); + mbstowcs(ret, utf8, wlen+1); + ret[wlen] = 0x0000; + } + + return ret; +} + +/* Get an attribute value from a udev_device and return it as a whar_t + string. The returned string must be freed with free() when done.*/ +static wchar_t *copy_udev_string(struct udev_device *dev, const char *udev_name) +{ + return utf8_to_wchar_t(udev_device_get_sysattr_value(dev, udev_name)); +} + +/* uses_numbered_reports() returns 1 if report_descriptor describes a device + which contains numbered reports. */ +static int uses_numbered_reports(__u8 *report_descriptor, __u32 size) { + unsigned int i = 0; + int size_code; + int data_len, key_size; + + while (i < size) { + int key = report_descriptor[i]; + + /* Check for the Report ID key */ + if (key == 0x85/*Report ID*/) { + /* This device has a Report ID, which means it uses + numbered reports. */ + return 1; + } + + //printf("key: %02hhx\n", key); + + if ((key & 0xf0) == 0xf0) { + /* This is a Long Item. The next byte contains the + length of the data section (value) for this key. + See the HID specification, version 1.11, section + 6.2.2.3, titled "Long Items." */ + if (i+1 < size) + data_len = report_descriptor[i+1]; + else + data_len = 0; /* malformed report */ + key_size = 3; + } + else { + /* This is a Short Item. The bottom two bits of the + key contain the size code for the data section + (value) for this key. Refer to the HID + specification, version 1.11, section 6.2.2.2, + titled "Short Items." */ + size_code = key & 0x3; + switch (size_code) { + case 0: + case 1: + case 2: + data_len = size_code; + break; + case 3: + data_len = 4; + break; + default: + /* Can't ever happen since size_code is & 0x3 */ + data_len = 0; + break; + }; + key_size = 1; + } + + /* Skip over this key and it's associated data */ + i += data_len + key_size; + } + + /* Didn't find a Report ID key. Device doesn't use numbered reports. */ + return 0; +} + +/* + * The caller is responsible for free()ing the (newly-allocated) character + * strings pointed to by serial_number_utf8 and product_name_utf8 after use. + */ +static int +parse_uevent_info(const char *uevent, int *bus_type, + unsigned short *vendor_id, unsigned short *product_id, + char **serial_number_utf8, char **product_name_utf8) +{ + char *tmp = strdup(uevent); + char *saveptr = NULL; + char *line; + char *key; + char *value; + + int found_id = 0; + int found_serial = 0; + int found_name = 0; + + line = strtok_r(tmp, "\n", &saveptr); + while (line != NULL) { + /* line: "KEY=value" */ + key = line; + value = strchr(line, '='); + if (!value) { + goto next_line; + } + *value = '\0'; + value++; + + if (strcmp(key, "HID_ID") == 0) { + /** + * type vendor product + * HID_ID=0003:000005AC:00008242 + **/ + int ret = sscanf(value, "%x:%hx:%hx", bus_type, vendor_id, product_id); + if (ret == 3) { + found_id = 1; + } + } else if (strcmp(key, "HID_NAME") == 0) { + /* The caller has to free the product name */ + *product_name_utf8 = strdup(value); + found_name = 1; + } else if (strcmp(key, "HID_UNIQ") == 0) { + /* The caller has to free the serial number */ + *serial_number_utf8 = strdup(value); + found_serial = 1; + } + +next_line: + line = strtok_r(NULL, "\n", &saveptr); + } + + free(tmp); + return (found_id && found_name && found_serial); +} + + +static int get_device_string(hid_device *dev, enum device_string_id key, wchar_t *string, size_t maxlen) +{ + struct udev *udev; + struct udev_device *udev_dev, *parent, *hid_dev; + struct stat s; + int ret = -1; + char *serial_number_utf8 = NULL; + char *product_name_utf8 = NULL; + + /* Create the udev object */ + udev = udev_new(); + if (!udev) { + printf("Can't create udev\n"); + return -1; + } + + /* Get the dev_t (major/minor numbers) from the file handle. */ + ret = fstat(dev->device_handle, &s); + if (-1 == ret) + return ret; + /* Open a udev device from the dev_t. 'c' means character device. */ + udev_dev = udev_device_new_from_devnum(udev, 'c', s.st_rdev); + if (udev_dev) { + hid_dev = udev_device_get_parent_with_subsystem_devtype( + udev_dev, + "hid", + NULL); + if (hid_dev) { + unsigned short dev_vid; + unsigned short dev_pid; + int bus_type; + size_t retm; + + ret = parse_uevent_info( + udev_device_get_sysattr_value(hid_dev, "uevent"), + &bus_type, + &dev_vid, + &dev_pid, + &serial_number_utf8, + &product_name_utf8); + + if (bus_type == BUS_BLUETOOTH) { + switch (key) { + case DEVICE_STRING_MANUFACTURER: + wcsncpy(string, L"", maxlen); + ret = 0; + break; + case DEVICE_STRING_PRODUCT: + retm = mbstowcs(string, product_name_utf8, maxlen); + ret = (retm == (size_t)-1)? -1: 0; + break; + case DEVICE_STRING_SERIAL: + retm = mbstowcs(string, serial_number_utf8, maxlen); + ret = (retm == (size_t)-1)? -1: 0; + break; + case DEVICE_STRING_COUNT: + default: + ret = -1; + break; + } + } + else { + /* This is a USB device. Find its parent USB Device node. */ + parent = udev_device_get_parent_with_subsystem_devtype( + udev_dev, + "usb", + "usb_device"); + if (parent) { + const char *str; + const char *key_str = NULL; + + if (key >= 0 && key < DEVICE_STRING_COUNT) { + key_str = device_string_names[key]; + } else { + ret = -1; + goto end; + } + + str = udev_device_get_sysattr_value(parent, key_str); + if (str) { + /* Convert the string from UTF-8 to wchar_t */ + retm = mbstowcs(string, str, maxlen); + ret = (retm == (size_t)-1)? -1: 0; + goto end; + } + } + } + } + } + +end: + free(serial_number_utf8); + free(product_name_utf8); + + udev_device_unref(udev_dev); + /* parent and hid_dev don't need to be (and can't be) unref'd. + I'm not sure why, but they'll throw double-free() errors. */ + udev_unref(udev); + + return ret; +} + +int HID_API_EXPORT hid_init(void) +{ + const char *locale; + + /* Set the locale if it's not set. */ + locale = setlocale(LC_CTYPE, NULL); + if (!locale) + setlocale(LC_CTYPE, ""); + + kernel_version = detect_kernel_version(); + + return 0; +} + +int HID_API_EXPORT hid_exit(void) +{ + /* Nothing to do for this in the Linux/hidraw implementation. */ + return 0; +} + + +struct hid_device_info HID_API_EXPORT *hid_enumerate(unsigned short vendor_id, unsigned short product_id) +{ + struct udev *udev; + struct udev_enumerate *enumerate; + struct udev_list_entry *devices, *dev_list_entry; + + struct hid_device_info *root = NULL; /* return object */ + struct hid_device_info *cur_dev = NULL; + struct hid_device_info *prev_dev = NULL; /* previous device */ + + hid_init(); + + /* Create the udev object */ + udev = udev_new(); + if (!udev) { + printf("Can't create udev\n"); + return NULL; + } + + /* Create a list of the devices in the 'hidraw' subsystem. */ + enumerate = udev_enumerate_new(udev); + udev_enumerate_add_match_subsystem(enumerate, "hidraw"); + udev_enumerate_scan_devices(enumerate); + devices = udev_enumerate_get_list_entry(enumerate); + /* For each item, see if it matches the vid/pid, and if so + create a udev_device record for it */ + udev_list_entry_foreach(dev_list_entry, devices) { + const char *sysfs_path; + const char *dev_path; + const char *str; + struct udev_device *raw_dev; /* The device's hidraw udev node. */ + struct udev_device *hid_dev; /* The device's HID udev node. */ + struct udev_device *usb_dev; /* The device's USB udev node. */ + struct udev_device *intf_dev; /* The device's interface (in the USB sense). */ + unsigned short dev_vid; + unsigned short dev_pid; + char *serial_number_utf8 = NULL; + char *product_name_utf8 = NULL; + int bus_type; + int result; + + /* Get the filename of the /sys entry for the device + and create a udev_device object (dev) representing it */ + sysfs_path = udev_list_entry_get_name(dev_list_entry); + raw_dev = udev_device_new_from_syspath(udev, sysfs_path); + dev_path = udev_device_get_devnode(raw_dev); + + hid_dev = udev_device_get_parent_with_subsystem_devtype( + raw_dev, + "hid", + NULL); + + if (!hid_dev) { + /* Unable to find parent hid device. */ + goto next; + } + + result = parse_uevent_info( + udev_device_get_sysattr_value(hid_dev, "uevent"), + &bus_type, + &dev_vid, + &dev_pid, + &serial_number_utf8, + &product_name_utf8); + + if (!result) { + /* parse_uevent_info() failed for at least one field. */ + goto next; + } + + if (bus_type != BUS_USB && bus_type != BUS_BLUETOOTH) { + /* We only know how to handle USB and BT devices. */ + goto next; + } + + /* Check the VID/PID against the arguments */ + if ((vendor_id == 0x0 || vendor_id == dev_vid) && + (product_id == 0x0 || product_id == dev_pid)) { + struct hid_device_info *tmp; + + /* VID/PID match. Create the record. */ + tmp = malloc(sizeof(struct hid_device_info)); + if (cur_dev) { + cur_dev->next = tmp; + } + else { + root = tmp; + } + prev_dev = cur_dev; + cur_dev = tmp; + + /* Fill out the record */ + cur_dev->next = NULL; + cur_dev->path = dev_path? strdup(dev_path): NULL; + + /* VID/PID */ + cur_dev->vendor_id = dev_vid; + cur_dev->product_id = dev_pid; + + /* Serial Number */ + cur_dev->serial_number = utf8_to_wchar_t(serial_number_utf8); + + /* Release Number */ + cur_dev->release_number = 0x0; + + /* Interface Number */ + cur_dev->interface_number = -1; + + switch (bus_type) { + case BUS_USB: + /* The device pointed to by raw_dev contains information about + the hidraw device. In order to get information about the + USB device, get the parent device with the + subsystem/devtype pair of "usb"/"usb_device". This will + be several levels up the tree, but the function will find + it. */ + usb_dev = udev_device_get_parent_with_subsystem_devtype( + raw_dev, + "usb", + "usb_device"); + + if (!usb_dev) { + /* Free this device */ + free(cur_dev->serial_number); + free(cur_dev->path); + free(cur_dev); + + /* Take it off the device list. */ + if (prev_dev) { + prev_dev->next = NULL; + cur_dev = prev_dev; + } + else { + cur_dev = root = NULL; + } + + goto next; + } + + /* Manufacturer and Product strings */ + cur_dev->manufacturer_string = copy_udev_string(usb_dev, device_string_names[DEVICE_STRING_MANUFACTURER]); + cur_dev->product_string = copy_udev_string(usb_dev, device_string_names[DEVICE_STRING_PRODUCT]); + + /* Release Number */ + str = udev_device_get_sysattr_value(usb_dev, "bcdDevice"); + cur_dev->release_number = (str)? strtol(str, NULL, 16): 0x0; + + /* Get a handle to the interface's udev node. */ + intf_dev = udev_device_get_parent_with_subsystem_devtype( + raw_dev, + "usb", + "usb_interface"); + if (intf_dev) { + str = udev_device_get_sysattr_value(intf_dev, "bInterfaceNumber"); + cur_dev->interface_number = (str)? strtol(str, NULL, 16): -1; + } + + break; + + case BUS_BLUETOOTH: + /* Manufacturer and Product strings */ + cur_dev->manufacturer_string = wcsdup(L""); + cur_dev->product_string = utf8_to_wchar_t(product_name_utf8); + + break; + + default: + /* Unknown device type - this should never happen, as we + * check for USB and Bluetooth devices above */ + break; + } + } + + next: + free(serial_number_utf8); + free(product_name_utf8); + udev_device_unref(raw_dev); + /* hid_dev, usb_dev and intf_dev don't need to be (and can't be) + unref()d. It will cause a double-free() error. I'm not + sure why. */ + } + /* Free the enumerator and udev objects. */ + udev_enumerate_unref(enumerate); + udev_unref(udev); + + return root; +} + +void HID_API_EXPORT hid_free_enumeration(struct hid_device_info *devs) +{ + struct hid_device_info *d = devs; + while (d) { + struct hid_device_info *next = d->next; + free(d->path); + free(d->serial_number); + free(d->manufacturer_string); + free(d->product_string); + free(d); + d = next; + } +} + +hid_device * hid_open(unsigned short vendor_id, unsigned short product_id, const wchar_t *serial_number) +{ + struct hid_device_info *devs, *cur_dev; + const char *path_to_open = NULL; + hid_device *handle = NULL; + + devs = hid_enumerate(vendor_id, product_id); + cur_dev = devs; + while (cur_dev) { + if (cur_dev->vendor_id == vendor_id && + cur_dev->product_id == product_id) { + if (serial_number) { + if (wcscmp(serial_number, cur_dev->serial_number) == 0) { + path_to_open = cur_dev->path; + break; + } + } + else { + path_to_open = cur_dev->path; + break; + } + } + cur_dev = cur_dev->next; + } + + if (path_to_open) { + /* Open the device */ + handle = hid_open_path(path_to_open); + } + + hid_free_enumeration(devs); + + return handle; +} + +hid_device * HID_API_EXPORT hid_open_path(const char *path) +{ + hid_device *dev = NULL; + + hid_init(); + + dev = new_hid_device(); + + /* OPEN HERE */ + dev->device_handle = open(path, O_RDWR); + + /* If we have a good handle, return it. */ + if (dev->device_handle > 0) { + + /* Get the report descriptor */ + int res, desc_size = 0; + struct hidraw_report_descriptor rpt_desc; + + memset(&rpt_desc, 0x0, sizeof(rpt_desc)); + + /* Get Report Descriptor Size */ + res = ioctl(dev->device_handle, HIDIOCGRDESCSIZE, &desc_size); + if (res < 0) + perror("HIDIOCGRDESCSIZE"); + + + /* Get Report Descriptor */ + rpt_desc.size = desc_size; + res = ioctl(dev->device_handle, HIDIOCGRDESC, &rpt_desc); + if (res < 0) { + perror("HIDIOCGRDESC"); + } else { + /* Determine if this device uses numbered reports. */ + dev->uses_numbered_reports = + uses_numbered_reports(rpt_desc.value, + rpt_desc.size); + } + + return dev; + } + else { + /* Unable to open any devices. */ + free(dev); + return NULL; + } +} + + +int HID_API_EXPORT hid_write(hid_device *dev, const unsigned char *data, size_t length) +{ + int bytes_written; + + bytes_written = write(dev->device_handle, data, length); + + return bytes_written; +} + + +int HID_API_EXPORT hid_read_timeout(hid_device *dev, unsigned char *data, size_t length, int milliseconds) +{ + int bytes_read; + + if (milliseconds >= 0) { + /* Milliseconds is either 0 (non-blocking) or > 0 (contains + a valid timeout). In both cases we want to call poll() + and wait for data to arrive. Don't rely on non-blocking + operation (O_NONBLOCK) since some kernels don't seem to + properly report device disconnection through read() when + in non-blocking mode. */ + int ret; + struct pollfd fds; + + fds.fd = dev->device_handle; + fds.events = POLLIN; + fds.revents = 0; + ret = poll(&fds, 1, milliseconds); + if (ret == -1 || ret == 0) { + /* Error or timeout */ + return ret; + } + else { + /* Check for errors on the file descriptor. This will + indicate a device disconnection. */ + if (fds.revents & (POLLERR | POLLHUP | POLLNVAL)) + return -1; + } + } + + bytes_read = read(dev->device_handle, data, length); + if (bytes_read < 0 && (errno == EAGAIN || errno == EINPROGRESS)) + bytes_read = 0; + + if (bytes_read >= 0 && + kernel_version != 0 && + kernel_version < KERNEL_VERSION(2,6,34) && + dev->uses_numbered_reports) { + /* Work around a kernel bug. Chop off the first byte. */ + memmove(data, data+1, bytes_read); + bytes_read--; + } + + return bytes_read; +} + +int HID_API_EXPORT hid_read(hid_device *dev, unsigned char *data, size_t length) +{ + return hid_read_timeout(dev, data, length, (dev->blocking)? -1: 0); +} + +int HID_API_EXPORT hid_set_nonblocking(hid_device *dev, int nonblock) +{ + /* Do all non-blocking in userspace using poll(), since it looks + like there's a bug in the kernel in some versions where + read() will not return -1 on disconnection of the USB device */ + + dev->blocking = !nonblock; + return 0; /* Success */ +} + + +int HID_API_EXPORT hid_send_feature_report(hid_device *dev, const unsigned char *data, size_t length) +{ + int res; + + res = ioctl(dev->device_handle, HIDIOCSFEATURE(length), data); + if (res < 0) + perror("ioctl (SFEATURE)"); + + return res; +} + +int HID_API_EXPORT hid_get_feature_report(hid_device *dev, unsigned char *data, size_t length) +{ + int res; + + res = ioctl(dev->device_handle, HIDIOCGFEATURE(length), data); + if (res < 0) + perror("ioctl (GFEATURE)"); + + + return res; +} + + +void HID_API_EXPORT hid_close(hid_device *dev) +{ + if (!dev) + return; + close(dev->device_handle); + free(dev); +} + + +int HID_API_EXPORT_CALL hid_get_manufacturer_string(hid_device *dev, wchar_t *string, size_t maxlen) +{ + return get_device_string(dev, DEVICE_STRING_MANUFACTURER, string, maxlen); +} + +int HID_API_EXPORT_CALL hid_get_product_string(hid_device *dev, wchar_t *string, size_t maxlen) +{ + return get_device_string(dev, DEVICE_STRING_PRODUCT, string, maxlen); +} + +int HID_API_EXPORT_CALL hid_get_serial_number_string(hid_device *dev, wchar_t *string, size_t maxlen) +{ + return get_device_string(dev, DEVICE_STRING_SERIAL, string, maxlen); +} + +int HID_API_EXPORT_CALL hid_get_indexed_string(hid_device *dev, int string_index, wchar_t *string, size_t maxlen) +{ + return -1; +} + + +HID_API_EXPORT const wchar_t * HID_API_CALL hid_error(hid_device *dev) +{ + return NULL; +} + +int HID_API_EXPORT_CALL hid_get_descriptor(hid_device *dev, unsigned char *data, size_t length) +{ + int res; + unsigned int desc_size = 0; + struct hidraw_report_descriptor rpt_desc; + + memset(&rpt_desc, 0x0, sizeof(rpt_desc)); + + /* Get Report Descriptor Size */ + res = ioctl(dev->device_handle, HIDIOCGRDESCSIZE, &desc_size); + if (res < 0) { + perror("HIDIOCGRDESCSIZE"); + return -1; + } + + // call with NULL buffer / 0 length to query size only + if ((data == NULL) || (length == 0)) + return desc_size; + + if (length < desc_size) { + perror("hid_get_descriptor: insufficent space for descriptor"); + return -1; + } + + /* Get Report Descriptor */ + rpt_desc.size = desc_size; + res = ioctl(dev->device_handle, HIDIOCGRDESC, &rpt_desc); + if (res < 0) { + perror("HIDIOCGRDESC"); + return -1; + } + + memcpy(data, rpt_desc.value, desc_size); + return desc_size; +} diff -Nru flightgear-2017.3.1+dfsg/3rdparty/hidapi/linux/README.txt flightgear-2018.1.1+dfsg/3rdparty/hidapi/linux/README.txt --- flightgear-2017.3.1+dfsg/3rdparty/hidapi/linux/README.txt 1970-01-01 00:00:00.000000000 +0000 +++ flightgear-2018.1.1+dfsg/3rdparty/hidapi/linux/README.txt 2018-04-06 19:43:33.000000000 +0000 @@ -0,0 +1,59 @@ + +There are two implementations of HIDAPI for Linux. One (linux/hid.c) uses the +Linux hidraw driver, and the other (libusb/hid.c) uses libusb. Which one you +use depends on your application. Complete functionality of the hidraw +version depends on patches to the Linux kernel which are not currently in +the mainline. These patches have to do with sending and receiving feature +reports. The libusb implementation uses libusb to talk directly to the +device, bypassing any Linux HID driver. The disadvantage of the libusb +version is that it will only work with USB devices, while the hidraw +implementation will work with Bluetooth devices as well. + +To use HIDAPI, simply drop either linux/hid.c or libusb/hid.c into your +application and build using the build parameters in the Makefile. + + +Libusb Implementation notes +---------------------------- +For the libusb implementation, libusb-1.0 must be installed. Libusb 1.0 is +different than the legacy libusb 0.1 which is installed on many systems. To +install libusb-1.0 on Ubuntu and other Debian-based systems, run: + sudo apt-get install libusb-1.0-0-dev + + +Hidraw Implementation notes +---------------------------- +For the hidraw implementation, libudev headers and libraries are required to +build hidapi programs. To install libudev libraries on Ubuntu, +and other Debian-based systems, run: + sudo apt-get install libudev-dev + +On Redhat-based systems, run the following as root: + yum install libudev-devel + +Unfortunately, the hidraw driver, which the linux version of hidapi is based +on, contains bugs in kernel versions < 2.6.36, which the client application +should be aware of. + +Bugs (hidraw implementation only): +----------------------------------- +On Kernel versions < 2.6.34, if your device uses numbered reports, an extra +byte will be returned at the beginning of all reports returned from read() +for hidraw devices. This is worked around in the libary. No action should be +necessary in the client library. + +On Kernel versions < 2.6.35, reports will only be sent using a Set_Report +transfer on the CONTROL endpoint. No data will ever be sent on an Interrupt +Out endpoint if one exists. This is fixed in 2.6.35. In 2.6.35, OUTPUT +reports will be sent to the device on the first INTERRUPT OUT endpoint if it +exists; If it does not exist, OUTPUT reports will be sent on the CONTROL +endpoint. + +On Kernel versions < 2.6.36, add an extra byte containing the report number +to sent reports if numbered reports are used, and the device does not +contain an INTERRPUT OUT endpoint for OUTPUT transfers. For example, if +your device uses numbered reports and wants to send {0x2 0xff 0xff 0xff} to +the device (0x2 is the report number), you must send {0x2 0x2 0xff 0xff +0xff}. If your device has the optional Interrupt OUT endpoint, this does not +apply (but really on 2.6.35 only, because 2.6.34 won't use the interrupt +out endpoint). diff -Nru flightgear-2017.3.1+dfsg/3rdparty/hidapi/mac/hid.c flightgear-2018.1.1+dfsg/3rdparty/hidapi/mac/hid.c --- flightgear-2017.3.1+dfsg/3rdparty/hidapi/mac/hid.c 1970-01-01 00:00:00.000000000 +0000 +++ flightgear-2018.1.1+dfsg/3rdparty/hidapi/mac/hid.c 2018-04-06 19:43:33.000000000 +0000 @@ -0,0 +1,1124 @@ +/******************************************************* + HIDAPI - Multi-Platform library for + communication with HID devices. + + Alan Ott + Signal 11 Software + + 2010-07-03 + + Copyright 2010, All Rights Reserved. + + At the discretion of the user of this library, + this software may be licensed under the terms of the + GNU General Public License v3, a BSD-Style license, or the + original HIDAPI license as outlined in the LICENSE.txt, + LICENSE-gpl3.txt, LICENSE-bsd.txt, and LICENSE-orig.txt + files located at the root of the source distribution. + These files may also be found in the public source + code repository located at: + http://github.com/signal11/hidapi . +********************************************************/ + +/* See Apple Technical Note TN2187 for details on IOHidManager. */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "hidapi.h" + +/* Barrier implementation because Mac OSX doesn't have pthread_barrier. + It also doesn't have clock_gettime(). So much for POSIX and SUSv2. + This implementation came from Brent Priddy and was posted on + StackOverflow. It is used with his permission. */ +typedef int pthread_barrierattr_t; +typedef struct pthread_barrier { + pthread_mutex_t mutex; + pthread_cond_t cond; + int count; + int trip_count; +} pthread_barrier_t; + +static int pthread_barrier_init(pthread_barrier_t *barrier, const pthread_barrierattr_t *attr, unsigned int count) +{ + if(count == 0) { + errno = EINVAL; + return -1; + } + + if(pthread_mutex_init(&barrier->mutex, 0) < 0) { + return -1; + } + if(pthread_cond_init(&barrier->cond, 0) < 0) { + pthread_mutex_destroy(&barrier->mutex); + return -1; + } + barrier->trip_count = count; + barrier->count = 0; + + return 0; +} + +static int pthread_barrier_destroy(pthread_barrier_t *barrier) +{ + pthread_cond_destroy(&barrier->cond); + pthread_mutex_destroy(&barrier->mutex); + return 0; +} + +static int pthread_barrier_wait(pthread_barrier_t *barrier) +{ + pthread_mutex_lock(&barrier->mutex); + ++(barrier->count); + if(barrier->count >= barrier->trip_count) + { + barrier->count = 0; + pthread_cond_broadcast(&barrier->cond); + pthread_mutex_unlock(&barrier->mutex); + return 1; + } + else + { + pthread_cond_wait(&barrier->cond, &(barrier->mutex)); + pthread_mutex_unlock(&barrier->mutex); + return 0; + } +} + +static int return_data(hid_device *dev, unsigned char *data, size_t length); + +/* Linked List of input reports received from the device. */ +struct input_report { + uint8_t *data; + size_t len; + struct input_report *next; +}; + +struct hid_device_ { + IOHIDDeviceRef device_handle; + int blocking; + int uses_numbered_reports; + int disconnected; + CFStringRef run_loop_mode; + CFRunLoopRef run_loop; + CFRunLoopSourceRef source; + uint8_t *input_report_buf; + CFIndex max_input_report_len; + struct input_report *input_reports; + + pthread_t thread; + pthread_mutex_t mutex; /* Protects input_reports */ + pthread_cond_t condition; + pthread_barrier_t barrier; /* Ensures correct startup sequence */ + pthread_barrier_t shutdown_barrier; /* Ensures correct shutdown sequence */ + int shutdown_thread; +}; + +static hid_device *new_hid_device(void) +{ + hid_device *dev = calloc(1, sizeof(hid_device)); + dev->device_handle = NULL; + dev->blocking = 1; + dev->uses_numbered_reports = 0; + dev->disconnected = 0; + dev->run_loop_mode = NULL; + dev->run_loop = NULL; + dev->source = NULL; + dev->input_report_buf = NULL; + dev->input_reports = NULL; + dev->shutdown_thread = 0; + + /* Thread objects */ + pthread_mutex_init(&dev->mutex, NULL); + pthread_cond_init(&dev->condition, NULL); + pthread_barrier_init(&dev->barrier, NULL, 2); + pthread_barrier_init(&dev->shutdown_barrier, NULL, 2); + + return dev; +} + +static void free_hid_device(hid_device *dev) +{ + if (!dev) + return; + + /* Delete any input reports still left over. */ + struct input_report *rpt = dev->input_reports; + while (rpt) { + struct input_report *next = rpt->next; + free(rpt->data); + free(rpt); + rpt = next; + } + + /* Free the string and the report buffer. The check for NULL + is necessary here as CFRelease() doesn't handle NULL like + free() and others do. */ + if (dev->run_loop_mode) + CFRelease(dev->run_loop_mode); + if (dev->source) + CFRelease(dev->source); + free(dev->input_report_buf); + + /* Clean up the thread objects */ + pthread_barrier_destroy(&dev->shutdown_barrier); + pthread_barrier_destroy(&dev->barrier); + pthread_cond_destroy(&dev->condition); + pthread_mutex_destroy(&dev->mutex); + + /* Free the structure itself. */ + free(dev); +} + +static IOHIDManagerRef hid_mgr = 0x0; + + +#if 0 +static void register_error(hid_device *device, const char *op) +{ + +} +#endif + + +static int32_t get_int_property(IOHIDDeviceRef device, CFStringRef key) +{ + CFTypeRef ref; + int32_t value; + + ref = IOHIDDeviceGetProperty(device, key); + if (ref) { + if (CFGetTypeID(ref) == CFNumberGetTypeID()) { + CFNumberGetValue((CFNumberRef) ref, kCFNumberSInt32Type, &value); + return value; + } + } + return 0; +} + +static unsigned short get_vendor_id(IOHIDDeviceRef device) +{ + return get_int_property(device, CFSTR(kIOHIDVendorIDKey)); +} + +static unsigned short get_product_id(IOHIDDeviceRef device) +{ + return get_int_property(device, CFSTR(kIOHIDProductIDKey)); +} + +static int32_t get_max_report_length(IOHIDDeviceRef device) +{ + return get_int_property(device, CFSTR(kIOHIDMaxInputReportSizeKey)); +} + +static int get_string_property(IOHIDDeviceRef device, CFStringRef prop, wchar_t *buf, size_t len) +{ + CFStringRef str; + + if (!len) + return 0; + + str = IOHIDDeviceGetProperty(device, prop); + + buf[0] = 0; + + if (str) { + CFIndex str_len = CFStringGetLength(str); + CFRange range; + CFIndex used_buf_len; + CFIndex chars_copied; + + len --; + + range.location = 0; + range.length = ((size_t)str_len > len)? len: (size_t)str_len; + chars_copied = CFStringGetBytes(str, + range, + kCFStringEncodingUTF32LE, + (char)'?', + FALSE, + (UInt8*)buf, + len * sizeof(wchar_t), + &used_buf_len); + + if (chars_copied == len) + buf[len] = 0; /* len is decremented above */ + else + buf[chars_copied] = 0; + + return 0; + } + else + return -1; + +} + +static int get_serial_number(IOHIDDeviceRef device, wchar_t *buf, size_t len) +{ + return get_string_property(device, CFSTR(kIOHIDSerialNumberKey), buf, len); +} + +static int get_manufacturer_string(IOHIDDeviceRef device, wchar_t *buf, size_t len) +{ + return get_string_property(device, CFSTR(kIOHIDManufacturerKey), buf, len); +} + +static int get_product_string(IOHIDDeviceRef device, wchar_t *buf, size_t len) +{ + return get_string_property(device, CFSTR(kIOHIDProductKey), buf, len); +} + + +/* Implementation of wcsdup() for Mac. */ +static wchar_t *dup_wcs(const wchar_t *s) +{ + size_t len = wcslen(s); + wchar_t *ret = malloc((len+1)*sizeof(wchar_t)); + wcscpy(ret, s); + + return ret; +} + +/* hidapi_IOHIDDeviceGetService() + * + * Return the io_service_t corresponding to a given IOHIDDeviceRef, either by: + * - on OS X 10.6 and above, calling IOHIDDeviceGetService() + * - on OS X 10.5, extract it from the IOHIDDevice struct + */ +static io_service_t hidapi_IOHIDDeviceGetService(IOHIDDeviceRef device) +{ + static void *iokit_framework = NULL; + static io_service_t (*dynamic_IOHIDDeviceGetService)(IOHIDDeviceRef device) = NULL; + + /* Use dlopen()/dlsym() to get a pointer to IOHIDDeviceGetService() if it exists. + * If any of these steps fail, dynamic_IOHIDDeviceGetService will be left NULL + * and the fallback method will be used. + */ + if (iokit_framework == NULL) { + iokit_framework = dlopen("/System/Library/IOKit.framework/IOKit", RTLD_LAZY); + + if (iokit_framework != NULL) + dynamic_IOHIDDeviceGetService = dlsym(iokit_framework, "IOHIDDeviceGetService"); + } + + if (dynamic_IOHIDDeviceGetService != NULL) { + /* Running on OS X 10.6 and above: IOHIDDeviceGetService() exists */ + return dynamic_IOHIDDeviceGetService(device); + } + else + { + /* Running on OS X 10.5: IOHIDDeviceGetService() doesn't exist. + * + * Be naughty and pull the service out of the IOHIDDevice. + * IOHIDDevice is an opaque struct not exposed to applications, but its + * layout is stable through all available versions of OS X. + * Tested and working on OS X 10.5.8 i386, x86_64, and ppc. + */ + struct IOHIDDevice_internal { + /* The first field of the IOHIDDevice struct is a + * CFRuntimeBase (which is a private CF struct). + * + * a, b, and c are the 3 fields that make up a CFRuntimeBase. + * See http://opensource.apple.com/source/CF/CF-476.18/CFRuntime.h + * + * The second field of the IOHIDDevice is the io_service_t we're looking for. + */ + uintptr_t a; + uint8_t b[4]; +#if __LP64__ + uint32_t c; +#endif + io_service_t service; + }; + struct IOHIDDevice_internal *tmp = (struct IOHIDDevice_internal *)device; + + return tmp->service; + } +} + +/* Initialize the IOHIDManager. Return 0 for success and -1 for failure. */ +static int init_hid_manager(void) +{ + /* Initialize all the HID Manager Objects */ + hid_mgr = IOHIDManagerCreate(kCFAllocatorDefault, kIOHIDOptionsTypeNone); + if (hid_mgr) { + IOHIDManagerSetDeviceMatching(hid_mgr, NULL); + IOHIDManagerScheduleWithRunLoop(hid_mgr, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode); + return 0; + } + + return -1; +} + +/* Initialize the IOHIDManager if necessary. This is the public function, and + it is safe to call this function repeatedly. Return 0 for success and -1 + for failure. */ +int HID_API_EXPORT hid_init(void) +{ + if (!hid_mgr) { + return init_hid_manager(); + } + + /* Already initialized. */ + return 0; +} + +int HID_API_EXPORT hid_exit(void) +{ + if (hid_mgr) { + /* Close the HID manager. */ + IOHIDManagerClose(hid_mgr, kIOHIDOptionsTypeNone); + CFRelease(hid_mgr); + hid_mgr = NULL; + } + + return 0; +} + +static void process_pending_events(void) { + SInt32 res; + do { + res = CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.001, FALSE); + } while(res != kCFRunLoopRunFinished && res != kCFRunLoopRunTimedOut); +} + +struct hid_device_info HID_API_EXPORT *hid_enumerate(unsigned short vendor_id, unsigned short product_id) +{ + struct hid_device_info *root = NULL; /* return object */ + struct hid_device_info *cur_dev = NULL; + CFIndex num_devices; + int i; + + /* Set up the HID Manager if it hasn't been done */ + if (hid_init() < 0) + return NULL; + + /* give the IOHIDManager a chance to update itself */ + process_pending_events(); + + /* Get a list of the Devices */ + IOHIDManagerSetDeviceMatching(hid_mgr, NULL); + CFSetRef device_set = IOHIDManagerCopyDevices(hid_mgr); + + /* Convert the list into a C array so we can iterate easily. */ + num_devices = CFSetGetCount(device_set); + IOHIDDeviceRef *device_array = calloc(num_devices, sizeof(IOHIDDeviceRef)); + CFSetGetValues(device_set, (const void **) device_array); + + /* Iterate over each device, making an entry for it. */ + for (i = 0; i < num_devices; i++) { + unsigned short dev_vid; + unsigned short dev_pid; + #define BUF_LEN 256 + wchar_t buf[BUF_LEN]; + + IOHIDDeviceRef dev = device_array[i]; + + if (!dev) { + continue; + } + dev_vid = get_vendor_id(dev); + dev_pid = get_product_id(dev); + + /* Check the VID/PID against the arguments */ + if ((vendor_id == 0x0 || vendor_id == dev_vid) && + (product_id == 0x0 || product_id == dev_pid)) { + struct hid_device_info *tmp; + io_object_t iokit_dev; + kern_return_t res; + io_string_t path; + + /* VID/PID match. Create the record. */ + tmp = malloc(sizeof(struct hid_device_info)); + if (cur_dev) { + cur_dev->next = tmp; + } + else { + root = tmp; + } + cur_dev = tmp; + + /* Get the Usage Page and Usage for this device. */ + cur_dev->usage_page = get_int_property(dev, CFSTR(kIOHIDPrimaryUsagePageKey)); + cur_dev->usage = get_int_property(dev, CFSTR(kIOHIDPrimaryUsageKey)); + + /* Fill out the record */ + cur_dev->next = NULL; + + /* Fill in the path (IOService plane) */ + iokit_dev = hidapi_IOHIDDeviceGetService(dev); + res = IORegistryEntryGetPath(iokit_dev, kIOServicePlane, path); + if (res == KERN_SUCCESS) + cur_dev->path = strdup(path); + else + cur_dev->path = strdup(""); + + /* Serial Number */ + get_serial_number(dev, buf, BUF_LEN); + cur_dev->serial_number = dup_wcs(buf); + + /* Manufacturer and Product strings */ + get_manufacturer_string(dev, buf, BUF_LEN); + cur_dev->manufacturer_string = dup_wcs(buf); + get_product_string(dev, buf, BUF_LEN); + cur_dev->product_string = dup_wcs(buf); + + /* VID/PID */ + cur_dev->vendor_id = dev_vid; + cur_dev->product_id = dev_pid; + + /* Release Number */ + cur_dev->release_number = get_int_property(dev, CFSTR(kIOHIDVersionNumberKey)); + + /* Interface Number (Unsupported on Mac)*/ + cur_dev->interface_number = -1; + } + } + + free(device_array); + CFRelease(device_set); + + return root; +} + +void HID_API_EXPORT hid_free_enumeration(struct hid_device_info *devs) +{ + /* This function is identical to the Linux version. Platform independent. */ + struct hid_device_info *d = devs; + while (d) { + struct hid_device_info *next = d->next; + free(d->path); + free(d->serial_number); + free(d->manufacturer_string); + free(d->product_string); + free(d); + d = next; + } +} + +hid_device * HID_API_EXPORT hid_open(unsigned short vendor_id, unsigned short product_id, const wchar_t *serial_number) +{ + /* This function is identical to the Linux version. Platform independent. */ + struct hid_device_info *devs, *cur_dev; + const char *path_to_open = NULL; + hid_device * handle = NULL; + + devs = hid_enumerate(vendor_id, product_id); + cur_dev = devs; + while (cur_dev) { + if (cur_dev->vendor_id == vendor_id && + cur_dev->product_id == product_id) { + if (serial_number) { + if (wcscmp(serial_number, cur_dev->serial_number) == 0) { + path_to_open = cur_dev->path; + break; + } + } + else { + path_to_open = cur_dev->path; + break; + } + } + cur_dev = cur_dev->next; + } + + if (path_to_open) { + /* Open the device */ + handle = hid_open_path(path_to_open); + } + + hid_free_enumeration(devs); + + return handle; +} + +static void hid_device_removal_callback(void *context, IOReturn result, + void *sender) +{ + /* Stop the Run Loop for this device. */ + hid_device *d = context; + + d->disconnected = 1; + CFRunLoopStop(d->run_loop); +} + +/* The Run Loop calls this function for each input report received. + This function puts the data into a linked list to be picked up by + hid_read(). */ +static void hid_report_callback(void *context, IOReturn result, void *sender, + IOHIDReportType report_type, uint32_t report_id, + uint8_t *report, CFIndex report_length) +{ + struct input_report *rpt; + hid_device *dev = context; + + /* Make a new Input Report object */ + rpt = calloc(1, sizeof(struct input_report)); + rpt->data = calloc(1, report_length); + memcpy(rpt->data, report, report_length); + rpt->len = report_length; + rpt->next = NULL; + + /* Lock this section */ + pthread_mutex_lock(&dev->mutex); + + /* Attach the new report object to the end of the list. */ + if (dev->input_reports == NULL) { + /* The list is empty. Put it at the root. */ + dev->input_reports = rpt; + } + else { + /* Find the end of the list and attach. */ + struct input_report *cur = dev->input_reports; + int num_queued = 0; + while (cur->next != NULL) { + cur = cur->next; + num_queued++; + } + cur->next = rpt; + + /* Pop one off if we've reached 30 in the queue. This + way we don't grow forever if the user never reads + anything from the device. */ + if (num_queued > 30) { + return_data(dev, NULL, 0); + } + } + + /* Signal a waiting thread that there is data. */ + pthread_cond_signal(&dev->condition); + + /* Unlock */ + pthread_mutex_unlock(&dev->mutex); + +} + +/* This gets called when the read_thread's run loop gets signaled by + hid_close(), and serves to stop the read_thread's run loop. */ +static void perform_signal_callback(void *context) +{ + hid_device *dev = context; + CFRunLoopStop(dev->run_loop); /*TODO: CFRunLoopGetCurrent()*/ +} + +static void *read_thread(void *param) +{ + hid_device *dev = param; + SInt32 code; + + /* Move the device's run loop to this thread. */ + IOHIDDeviceScheduleWithRunLoop(dev->device_handle, CFRunLoopGetCurrent(), dev->run_loop_mode); + + /* Create the RunLoopSource which is used to signal the + event loop to stop when hid_close() is called. */ + CFRunLoopSourceContext ctx; + memset(&ctx, 0, sizeof(ctx)); + ctx.version = 0; + ctx.info = dev; + ctx.perform = &perform_signal_callback; + dev->source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0/*order*/, &ctx); + CFRunLoopAddSource(CFRunLoopGetCurrent(), dev->source, dev->run_loop_mode); + + /* Store off the Run Loop so it can be stopped from hid_close() + and on device disconnection. */ + dev->run_loop = CFRunLoopGetCurrent(); + + /* Notify the main thread that the read thread is up and running. */ + pthread_barrier_wait(&dev->barrier); + + /* Run the Event Loop. CFRunLoopRunInMode() will dispatch HID input + reports into the hid_report_callback(). */ + while (!dev->shutdown_thread && !dev->disconnected) { + code = CFRunLoopRunInMode(dev->run_loop_mode, 1000/*sec*/, FALSE); + /* Return if the device has been disconnected */ + if (code == kCFRunLoopRunFinished) { + dev->disconnected = 1; + break; + } + + + /* Break if The Run Loop returns Finished or Stopped. */ + if (code != kCFRunLoopRunTimedOut && + code != kCFRunLoopRunHandledSource) { + /* There was some kind of error. Setting + shutdown seems to make sense, but + there may be something else more appropriate */ + dev->shutdown_thread = 1; + break; + } + } + + /* Now that the read thread is stopping, Wake any threads which are + waiting on data (in hid_read_timeout()). Do this under a mutex to + make sure that a thread which is about to go to sleep waiting on + the condition actually will go to sleep before the condition is + signaled. */ + pthread_mutex_lock(&dev->mutex); + pthread_cond_broadcast(&dev->condition); + pthread_mutex_unlock(&dev->mutex); + + /* Wait here until hid_close() is called and makes it past + the call to CFRunLoopWakeUp(). This thread still needs to + be valid when that function is called on the other thread. */ + pthread_barrier_wait(&dev->shutdown_barrier); + + return NULL; +} + +/* hid_open_path() + * + * path must be a valid path to an IOHIDDevice in the IOService plane + * Example: "IOService:/AppleACPIPlatformExpert/PCI0@0/AppleACPIPCI/EHC1@1D,7/AppleUSBEHCI/PLAYSTATION(R)3 Controller@fd120000/IOUSBInterface@0/IOUSBHIDDriver" + */ +hid_device * HID_API_EXPORT hid_open_path(const char *path) +{ + hid_device *dev = NULL; + io_registry_entry_t entry = MACH_PORT_NULL; + + dev = new_hid_device(); + + /* Set up the HID Manager if it hasn't been done */ + if (hid_init() < 0) + return NULL; + + /* Get the IORegistry entry for the given path */ + entry = IORegistryEntryFromPath(kIOMasterPortDefault, path); + if (entry == MACH_PORT_NULL) { + /* Path wasn't valid (maybe device was removed?) */ + goto return_error; + } + + /* Create an IOHIDDevice for the entry */ + dev->device_handle = IOHIDDeviceCreate(kCFAllocatorDefault, entry); + if (dev->device_handle == NULL) { + /* Error creating the HID device */ + goto return_error; + } + + /* Open the IOHIDDevice */ + IOReturn ret = IOHIDDeviceOpen(dev->device_handle, kIOHIDOptionsTypeSeizeDevice); + if (ret == kIOReturnSuccess) { + char str[32]; + + /* Create the buffers for receiving data */ + dev->max_input_report_len = (CFIndex) get_max_report_length(dev->device_handle); + dev->input_report_buf = calloc(dev->max_input_report_len, sizeof(uint8_t)); + + /* Create the Run Loop Mode for this device. + printing the reference seems to work. */ + sprintf(str, "HIDAPI_%p", dev->device_handle); + dev->run_loop_mode = + CFStringCreateWithCString(NULL, str, kCFStringEncodingASCII); + + /* Attach the device to a Run Loop */ + IOHIDDeviceRegisterInputReportCallback( + dev->device_handle, dev->input_report_buf, dev->max_input_report_len, + &hid_report_callback, dev); + IOHIDDeviceRegisterRemovalCallback(dev->device_handle, hid_device_removal_callback, dev); + + /* Start the read thread */ + pthread_create(&dev->thread, NULL, read_thread, dev); + + /* Wait here for the read thread to be initialized. */ + pthread_barrier_wait(&dev->barrier); + + IOObjectRelease(entry); + return dev; + } + else { + goto return_error; + } + +return_error: + if (dev->device_handle != NULL) + CFRelease(dev->device_handle); + + if (entry != MACH_PORT_NULL) + IOObjectRelease(entry); + + free_hid_device(dev); + return NULL; +} + +static int set_report(hid_device *dev, IOHIDReportType type, const unsigned char *data, size_t length) +{ + const unsigned char *data_to_send; + size_t length_to_send; + IOReturn res; + + /* Return if the device has been disconnected. */ + if (dev->disconnected) + return -1; + + if (data[0] == 0x0) { + /* Not using numbered Reports. + Don't send the report number. */ + data_to_send = data+1; + length_to_send = length-1; + } + else { + /* Using numbered Reports. + Send the Report Number */ + data_to_send = data; + length_to_send = length; + } + + if (!dev->disconnected) { + res = IOHIDDeviceSetReport(dev->device_handle, + type, + data[0], /* Report ID*/ + data_to_send, length_to_send); + + if (res == kIOReturnSuccess) { + return length; + } + else + return -1; + } + + return -1; +} + +int HID_API_EXPORT hid_write(hid_device *dev, const unsigned char *data, size_t length) +{ + return set_report(dev, kIOHIDReportTypeOutput, data, length); +} + +/* Helper function, so that this isn't duplicated in hid_read(). */ +static int return_data(hid_device *dev, unsigned char *data, size_t length) +{ + /* Copy the data out of the linked list item (rpt) into the + return buffer (data), and delete the liked list item. */ + struct input_report *rpt = dev->input_reports; + size_t len = (length < rpt->len)? length: rpt->len; + memcpy(data, rpt->data, len); + dev->input_reports = rpt->next; + free(rpt->data); + free(rpt); + return len; +} + +static int cond_wait(const hid_device *dev, pthread_cond_t *cond, pthread_mutex_t *mutex) +{ + while (!dev->input_reports) { + int res = pthread_cond_wait(cond, mutex); + if (res != 0) + return res; + + /* A res of 0 means we may have been signaled or it may + be a spurious wakeup. Check to see that there's acutally + data in the queue before returning, and if not, go back + to sleep. See the pthread_cond_timedwait() man page for + details. */ + + if (dev->shutdown_thread || dev->disconnected) + return -1; + } + + return 0; +} + +static int cond_timedwait(const hid_device *dev, pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime) +{ + while (!dev->input_reports) { + int res = pthread_cond_timedwait(cond, mutex, abstime); + if (res != 0) + return res; + + /* A res of 0 means we may have been signaled or it may + be a spurious wakeup. Check to see that there's acutally + data in the queue before returning, and if not, go back + to sleep. See the pthread_cond_timedwait() man page for + details. */ + + if (dev->shutdown_thread || dev->disconnected) + return -1; + } + + return 0; + +} + +int HID_API_EXPORT hid_read_timeout(hid_device *dev, unsigned char *data, size_t length, int milliseconds) +{ + int bytes_read = -1; + + /* Lock the access to the report list. */ + pthread_mutex_lock(&dev->mutex); + + /* There's an input report queued up. Return it. */ + if (dev->input_reports) { + /* Return the first one */ + bytes_read = return_data(dev, data, length); + goto ret; + } + + /* Return if the device has been disconnected. */ + if (dev->disconnected) { + bytes_read = -1; + goto ret; + } + + if (dev->shutdown_thread) { + /* This means the device has been closed (or there + has been an error. An error code of -1 should + be returned. */ + bytes_read = -1; + goto ret; + } + + /* There is no data. Go to sleep and wait for data. */ + + if (milliseconds == -1) { + /* Blocking */ + int res; + res = cond_wait(dev, &dev->condition, &dev->mutex); + if (res == 0) + bytes_read = return_data(dev, data, length); + else { + /* There was an error, or a device disconnection. */ + bytes_read = -1; + } + } + else if (milliseconds > 0) { + /* Non-blocking, but called with timeout. */ + int res; + struct timespec ts; + struct timeval tv; + gettimeofday(&tv, NULL); + TIMEVAL_TO_TIMESPEC(&tv, &ts); + ts.tv_sec += milliseconds / 1000; + ts.tv_nsec += (milliseconds % 1000) * 1000000; + if (ts.tv_nsec >= 1000000000L) { + ts.tv_sec++; + ts.tv_nsec -= 1000000000L; + } + + res = cond_timedwait(dev, &dev->condition, &dev->mutex, &ts); + if (res == 0) + bytes_read = return_data(dev, data, length); + else if (res == ETIMEDOUT) + bytes_read = 0; + else + bytes_read = -1; + } + else { + /* Purely non-blocking */ + bytes_read = 0; + } + +ret: + /* Unlock */ + pthread_mutex_unlock(&dev->mutex); + return bytes_read; +} + +int HID_API_EXPORT hid_read(hid_device *dev, unsigned char *data, size_t length) +{ + return hid_read_timeout(dev, data, length, (dev->blocking)? -1: 0); +} + +int HID_API_EXPORT hid_set_nonblocking(hid_device *dev, int nonblock) +{ + /* All Nonblocking operation is handled by the library. */ + dev->blocking = !nonblock; + + return 0; +} + +int HID_API_EXPORT hid_send_feature_report(hid_device *dev, const unsigned char *data, size_t length) +{ + return set_report(dev, kIOHIDReportTypeFeature, data, length); +} + +int HID_API_EXPORT hid_get_feature_report(hid_device *dev, unsigned char *data, size_t length) +{ + CFIndex len = length; + IOReturn res; + + /* Return if the device has been unplugged. */ + if (dev->disconnected) + return -1; + + res = IOHIDDeviceGetReport(dev->device_handle, + kIOHIDReportTypeFeature, + data[0], /* Report ID */ + data, &len); + if (res == kIOReturnSuccess) + return len; + else + return -1; +} + + +void HID_API_EXPORT hid_close(hid_device *dev) +{ + if (!dev) + return; + + /* Disconnect the report callback before close. */ + if (!dev->disconnected) { + IOHIDDeviceRegisterInputReportCallback( + dev->device_handle, dev->input_report_buf, dev->max_input_report_len, + NULL, dev); + IOHIDDeviceRegisterRemovalCallback(dev->device_handle, NULL, dev); + IOHIDDeviceUnscheduleFromRunLoop(dev->device_handle, dev->run_loop, dev->run_loop_mode); + IOHIDDeviceScheduleWithRunLoop(dev->device_handle, CFRunLoopGetMain(), kCFRunLoopDefaultMode); + } + + /* Cause read_thread() to stop. */ + dev->shutdown_thread = 1; + + /* Wake up the run thread's event loop so that the thread can exit. */ + CFRunLoopSourceSignal(dev->source); + CFRunLoopWakeUp(dev->run_loop); + + /* Notify the read thread that it can shut down now. */ + pthread_barrier_wait(&dev->shutdown_barrier); + + /* Wait for read_thread() to end. */ + pthread_join(dev->thread, NULL); + + /* Close the OS handle to the device, but only if it's not + been unplugged. If it's been unplugged, then calling + IOHIDDeviceClose() will crash. */ + if (!dev->disconnected) { + IOHIDDeviceClose(dev->device_handle, kIOHIDOptionsTypeSeizeDevice); + } + + /* Clear out the queue of received reports. */ + pthread_mutex_lock(&dev->mutex); + while (dev->input_reports) { + return_data(dev, NULL, 0); + } + pthread_mutex_unlock(&dev->mutex); + CFRelease(dev->device_handle); + + free_hid_device(dev); +} + +int HID_API_EXPORT_CALL hid_get_manufacturer_string(hid_device *dev, wchar_t *string, size_t maxlen) +{ + return get_manufacturer_string(dev->device_handle, string, maxlen); +} + +int HID_API_EXPORT_CALL hid_get_product_string(hid_device *dev, wchar_t *string, size_t maxlen) +{ + return get_product_string(dev->device_handle, string, maxlen); +} + +int HID_API_EXPORT_CALL hid_get_serial_number_string(hid_device *dev, wchar_t *string, size_t maxlen) +{ + return get_serial_number(dev->device_handle, string, maxlen); +} + +int HID_API_EXPORT_CALL hid_get_indexed_string(hid_device *dev, int string_index, wchar_t *string, size_t maxlen) +{ + /* TODO: */ + + return 0; +} + + +HID_API_EXPORT const wchar_t * HID_API_CALL hid_error(hid_device *dev) +{ + /* TODO: */ + + return NULL; +} + + + +int HID_API_EXPORT hid_get_descriptor(hid_device *dev, unsigned char *data, size_t length) +{ + /* Return if the device has been unplugged. */ + if (dev->disconnected) + return -1; + + CFTypeRef ref; + ref = IOHIDDeviceGetProperty(dev->device_handle, CFSTR(kIOHIDReportDescriptorKey)); + if (ref) { + CFIndex len = CFDataGetLength(ref); + if (length < len) len = length; + CFDataGetBytes(ref, CFRangeMake(0,len), data); + return len; + } + + return -1; +} + +#if 0 +static int32_t get_location_id(IOHIDDeviceRef device) +{ + return get_int_property(device, CFSTR(kIOHIDLocationIDKey)); +} + +static int32_t get_usage(IOHIDDeviceRef device) +{ + int32_t res; + res = get_int_property(device, CFSTR(kIOHIDDeviceUsageKey)); + if (!res) + res = get_int_property(device, CFSTR(kIOHIDPrimaryUsageKey)); + return res; +} + +static int32_t get_usage_page(IOHIDDeviceRef device) +{ + int32_t res; + res = get_int_property(device, CFSTR(kIOHIDDeviceUsagePageKey)); + if (!res) + res = get_int_property(device, CFSTR(kIOHIDPrimaryUsagePageKey)); + return res; +} + +static int get_transport(IOHIDDeviceRef device, wchar_t *buf, size_t len) +{ + return get_string_property(device, CFSTR(kIOHIDTransportKey), buf, len); +} + + +int main(void) +{ + IOHIDManagerRef mgr; + int i; + + mgr = IOHIDManagerCreate(kCFAllocatorDefault, kIOHIDOptionsTypeNone); + IOHIDManagerSetDeviceMatching(mgr, NULL); + IOHIDManagerOpen(mgr, kIOHIDOptionsTypeNone); + + CFSetRef device_set = IOHIDManagerCopyDevices(mgr); + + CFIndex num_devices = CFSetGetCount(device_set); + IOHIDDeviceRef *device_array = calloc(num_devices, sizeof(IOHIDDeviceRef)); + CFSetGetValues(device_set, (const void **) device_array); + + for (i = 0; i < num_devices; i++) { + IOHIDDeviceRef dev = device_array[i]; + printf("Device: %p\n", dev); + printf(" %04hx %04hx\n", get_vendor_id(dev), get_product_id(dev)); + + wchar_t serial[256], buf[256]; + char cbuf[256]; + get_serial_number(dev, serial, 256); + + + printf(" Serial: %ls\n", serial); + printf(" Loc: %ld\n", get_location_id(dev)); + get_transport(dev, buf, 256); + printf(" Trans: %ls\n", buf); + make_path(dev, cbuf, 256); + printf(" Path: %s\n", cbuf); + + } + + return 0; +} +#endif diff -Nru flightgear-2017.3.1+dfsg/3rdparty/hidapi/windows/ddk_build/hidapi.def flightgear-2018.1.1+dfsg/3rdparty/hidapi/windows/ddk_build/hidapi.def --- flightgear-2017.3.1+dfsg/3rdparty/hidapi/windows/ddk_build/hidapi.def 1970-01-01 00:00:00.000000000 +0000 +++ flightgear-2018.1.1+dfsg/3rdparty/hidapi/windows/ddk_build/hidapi.def 2018-04-06 19:43:33.000000000 +0000 @@ -0,0 +1,17 @@ +LIBRARY hidapi +EXPORTS + hid_open @1 + hid_write @2 + hid_read @3 + hid_close @4 + hid_get_product_string @5 + hid_get_manufacturer_string @6 + hid_get_serial_number_string @7 + hid_get_indexed_string @8 + hid_error @9 + hid_set_nonblocking @10 + hid_enumerate @11 + hid_open_path @12 + hid_send_feature_report @13 + hid_get_feature_report @14 + \ No newline at end of file diff -Nru flightgear-2017.3.1+dfsg/3rdparty/hidapi/windows/ddk_build/sources flightgear-2018.1.1+dfsg/3rdparty/hidapi/windows/ddk_build/sources --- flightgear-2017.3.1+dfsg/3rdparty/hidapi/windows/ddk_build/sources 1970-01-01 00:00:00.000000000 +0000 +++ flightgear-2018.1.1+dfsg/3rdparty/hidapi/windows/ddk_build/sources 2018-04-06 19:43:33.000000000 +0000 @@ -0,0 +1,23 @@ +TARGETNAME=hidapi +TARGETTYPE=DYNLINK +UMTYPE=console +UMENTRY=main + +MSC_WARNING_LEVEL=/W3 /WX + +TARGETLIBS=$(SDK_LIB_PATH)\hid.lib \ + $(SDK_LIB_PATH)\setupapi.lib \ + $(SDK_LIB_PATH)\kernel32.lib \ + $(SDK_LIB_PATH)\comdlg32.lib + +USE_MSVCRT=1 + +INCLUDES= ..\..\hidapi +SOURCES= ..\hid.c \ + + +TARGET_DESTINATION=retail + +MUI=0 +MUI_COMMENT="HID Interface DLL" + diff -Nru flightgear-2017.3.1+dfsg/3rdparty/hidapi/windows/hid.c flightgear-2018.1.1+dfsg/3rdparty/hidapi/windows/hid.c --- flightgear-2017.3.1+dfsg/3rdparty/hidapi/windows/hid.c 1970-01-01 00:00:00.000000000 +0000 +++ flightgear-2018.1.1+dfsg/3rdparty/hidapi/windows/hid.c 2018-04-06 19:43:33.000000000 +0000 @@ -0,0 +1,981 @@ +/******************************************************* + HIDAPI - Multi-Platform library for + communication with HID devices. + + Alan Ott + Signal 11 Software + + 8/22/2009 + + Copyright 2009, All Rights Reserved. + + At the discretion of the user of this library, + this software may be licensed under the terms of the + GNU General Public License v3, a BSD-Style license, or the + original HIDAPI license as outlined in the LICENSE.txt, + LICENSE-gpl3.txt, LICENSE-bsd.txt, and LICENSE-orig.txt + files located at the root of the source distribution. + These files may also be found in the public source + code repository located at: + http://github.com/signal11/hidapi . +********************************************************/ + +#include + +#ifndef _NTDEF_ +typedef LONG NTSTATUS; +#endif + +#ifdef __MINGW32__ +#include +#include +#endif + +#ifdef __CYGWIN__ +#include +#define _wcsdup wcsdup +#endif + +/* The maximum number of characters that can be passed into the + HidD_Get*String() functions without it failing.*/ +#define MAX_STRING_WCHARS 0xFFF + +/*#define HIDAPI_USE_DDK*/ + +#ifdef __cplusplus +extern "C" { +#endif + #include + #include + #ifdef HIDAPI_USE_DDK + #include + #endif + + /* Copied from inc/ddk/hidclass.h, part of the Windows DDK. */ + #define HID_OUT_CTL_CODE(id) \ + CTL_CODE(FILE_DEVICE_KEYBOARD, (id), METHOD_OUT_DIRECT, FILE_ANY_ACCESS) + #define IOCTL_HID_GET_FEATURE HID_OUT_CTL_CODE(100) + + #define HID_CTL_CODE(id) \ + CTL_CODE(FILE_DEVICE_KEYBOARD, (id), METHOD_NEITHER, FILE_ANY_ACCESS) + #define IOCTL_HID_GET_REPORT_DESCRIPTOR HID_CTL_CODE(1) + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#include +#include + + +#include "hidapi.h" + +#undef MIN +#define MIN(x,y) ((x) < (y)? (x): (y)) + +#ifdef _MSC_VER + /* Thanks Microsoft, but I know how to use strncpy(). */ + #pragma warning(disable:4996) +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +#ifndef HIDAPI_USE_DDK + /* Since we're not building with the DDK, and the HID header + files aren't part of the SDK, we have to define all this + stuff here. In lookup_functions(), the function pointers + defined below are set. */ + typedef struct _HIDD_ATTRIBUTES{ + ULONG Size; + USHORT VendorID; + USHORT ProductID; + USHORT VersionNumber; + } HIDD_ATTRIBUTES, *PHIDD_ATTRIBUTES; + + typedef USHORT USAGE; + typedef struct _HIDP_CAPS { + USAGE Usage; + USAGE UsagePage; + USHORT InputReportByteLength; + USHORT OutputReportByteLength; + USHORT FeatureReportByteLength; + USHORT Reserved[17]; + USHORT fields_not_used_by_hidapi[10]; + } HIDP_CAPS, *PHIDP_CAPS; + typedef void* PHIDP_PREPARSED_DATA; + #define HIDP_STATUS_SUCCESS 0x110000 + + typedef BOOLEAN (__stdcall *HidD_GetAttributes_)(HANDLE device, PHIDD_ATTRIBUTES attrib); + typedef BOOLEAN (__stdcall *HidD_GetSerialNumberString_)(HANDLE device, PVOID buffer, ULONG buffer_len); + typedef BOOLEAN (__stdcall *HidD_GetManufacturerString_)(HANDLE handle, PVOID buffer, ULONG buffer_len); + typedef BOOLEAN (__stdcall *HidD_GetProductString_)(HANDLE handle, PVOID buffer, ULONG buffer_len); + typedef BOOLEAN (__stdcall *HidD_SetFeature_)(HANDLE handle, PVOID data, ULONG length); + typedef BOOLEAN (__stdcall *HidD_GetFeature_)(HANDLE handle, PVOID data, ULONG length); + typedef BOOLEAN (__stdcall *HidD_GetIndexedString_)(HANDLE handle, ULONG string_index, PVOID buffer, ULONG buffer_len); + typedef BOOLEAN (__stdcall *HidD_GetPreparsedData_)(HANDLE handle, PHIDP_PREPARSED_DATA *preparsed_data); + typedef BOOLEAN (__stdcall *HidD_FreePreparsedData_)(PHIDP_PREPARSED_DATA preparsed_data); + typedef NTSTATUS (__stdcall *HidP_GetCaps_)(PHIDP_PREPARSED_DATA preparsed_data, HIDP_CAPS *caps); + typedef BOOLEAN (__stdcall *HidD_SetNumInputBuffers_)(HANDLE handle, ULONG number_buffers); + + static HidD_GetAttributes_ HidD_GetAttributes; + static HidD_GetSerialNumberString_ HidD_GetSerialNumberString; + static HidD_GetManufacturerString_ HidD_GetManufacturerString; + static HidD_GetProductString_ HidD_GetProductString; + static HidD_SetFeature_ HidD_SetFeature; + static HidD_GetFeature_ HidD_GetFeature; + static HidD_GetIndexedString_ HidD_GetIndexedString; + static HidD_GetPreparsedData_ HidD_GetPreparsedData; + static HidD_FreePreparsedData_ HidD_FreePreparsedData; + static HidP_GetCaps_ HidP_GetCaps; + static HidD_SetNumInputBuffers_ HidD_SetNumInputBuffers; + + static HMODULE lib_handle = NULL; + static BOOLEAN initialized = FALSE; +#endif /* HIDAPI_USE_DDK */ + +struct hid_device_ { + HANDLE device_handle; + BOOL blocking; + USHORT output_report_length; + size_t input_report_length; + void *last_error_str; + DWORD last_error_num; + BOOL read_pending; + char *read_buf; + OVERLAPPED ol; +}; + +static hid_device *new_hid_device() +{ + hid_device *dev = (hid_device*) calloc(1, sizeof(hid_device)); + dev->device_handle = INVALID_HANDLE_VALUE; + dev->blocking = TRUE; + dev->output_report_length = 0; + dev->input_report_length = 0; + dev->last_error_str = NULL; + dev->last_error_num = 0; + dev->read_pending = FALSE; + dev->read_buf = NULL; + memset(&dev->ol, 0, sizeof(dev->ol)); + dev->ol.hEvent = CreateEvent(NULL, FALSE, FALSE /*initial state f=nonsignaled*/, NULL); + + return dev; +} + +static void free_hid_device(hid_device *dev) +{ + CloseHandle(dev->ol.hEvent); + CloseHandle(dev->device_handle); + LocalFree(dev->last_error_str); + free(dev->read_buf); + free(dev); +} + +static void register_error(hid_device *device, const char *op) +{ + WCHAR *ptr, *msg; + + FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER | + FORMAT_MESSAGE_FROM_SYSTEM | + FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, + GetLastError(), + MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + (LPVOID)&msg, 0/*sz*/, + NULL); + + /* Get rid of the CR and LF that FormatMessage() sticks at the + end of the message. Thanks Microsoft! */ + ptr = msg; + + while (*ptr) { + if (*ptr == '\r') { + *ptr = 0x0000; + break; + } + ptr++; + } + + /* Store the message off in the Device entry so that + the hid_error() function can pick it up. */ + LocalFree(device->last_error_str); + device->last_error_str = msg; +} + +#ifndef HIDAPI_USE_DDK +static int lookup_functions() +{ + lib_handle = LoadLibraryA("hid.dll"); + if (lib_handle) { +#define RESOLVE(x) x = (x##_)GetProcAddress(lib_handle, #x); if (!x) return -1; + RESOLVE(HidD_GetAttributes); + RESOLVE(HidD_GetSerialNumberString); + RESOLVE(HidD_GetManufacturerString); + RESOLVE(HidD_GetProductString); + RESOLVE(HidD_SetFeature); + RESOLVE(HidD_GetFeature); + RESOLVE(HidD_GetIndexedString); + RESOLVE(HidD_GetPreparsedData); + RESOLVE(HidD_FreePreparsedData); + RESOLVE(HidP_GetCaps); + RESOLVE(HidD_SetNumInputBuffers); +#undef RESOLVE + } + else + return -1; + + return 0; +} +#endif + +static HANDLE open_device(const char *path, BOOL enumerate) +{ + HANDLE handle; + DWORD desired_access = (enumerate)? 0: (GENERIC_WRITE | GENERIC_READ); + DWORD share_mode = FILE_SHARE_READ|FILE_SHARE_WRITE; + + handle = CreateFileA(path, + desired_access, + share_mode, + NULL, + OPEN_EXISTING, + FILE_FLAG_OVERLAPPED,/*FILE_ATTRIBUTE_NORMAL,*/ + 0); + + return handle; +} + +int HID_API_EXPORT hid_init(void) +{ +#ifndef HIDAPI_USE_DDK + if (!initialized) { + if (lookup_functions() < 0) { + hid_exit(); + return -1; + } + initialized = TRUE; + } +#endif + return 0; +} + +int HID_API_EXPORT hid_exit(void) +{ +#ifndef HIDAPI_USE_DDK + if (lib_handle) + FreeLibrary(lib_handle); + lib_handle = NULL; + initialized = FALSE; +#endif + return 0; +} + +struct hid_device_info HID_API_EXPORT * HID_API_CALL hid_enumerate(unsigned short vendor_id, unsigned short product_id) +{ + BOOL res; + struct hid_device_info *root = NULL; /* return object */ + struct hid_device_info *cur_dev = NULL; + + /* Windows objects for interacting with the driver. */ + GUID InterfaceClassGuid = {0x4d1e55b2, 0xf16f, 0x11cf, {0x88, 0xcb, 0x00, 0x11, 0x11, 0x00, 0x00, 0x30} }; + SP_DEVINFO_DATA devinfo_data; + SP_DEVICE_INTERFACE_DATA device_interface_data; + SP_DEVICE_INTERFACE_DETAIL_DATA_A *device_interface_detail_data = NULL; + HDEVINFO device_info_set = INVALID_HANDLE_VALUE; + int device_index = 0; + int i; + + if (hid_init() < 0) + return NULL; + + /* Initialize the Windows objects. */ + memset(&devinfo_data, 0x0, sizeof(devinfo_data)); + devinfo_data.cbSize = sizeof(SP_DEVINFO_DATA); + device_interface_data.cbSize = sizeof(SP_DEVICE_INTERFACE_DATA); + + /* Get information for all the devices belonging to the HID class. */ + device_info_set = SetupDiGetClassDevsA(&InterfaceClassGuid, NULL, NULL, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE); + + /* Iterate over each device in the HID class, looking for the right one. */ + + for (;;) { + HANDLE write_handle = INVALID_HANDLE_VALUE; + DWORD required_size = 0; + HIDD_ATTRIBUTES attrib; + + res = SetupDiEnumDeviceInterfaces(device_info_set, + NULL, + &InterfaceClassGuid, + device_index, + &device_interface_data); + + if (!res) { + /* A return of FALSE from this function means that + there are no more devices. */ + break; + } + + /* Call with 0-sized detail size, and let the function + tell us how long the detail struct needs to be. The + size is put in &required_size. */ + res = SetupDiGetDeviceInterfaceDetailA(device_info_set, + &device_interface_data, + NULL, + 0, + &required_size, + NULL); + + /* Allocate a long enough structure for device_interface_detail_data. */ + device_interface_detail_data = (SP_DEVICE_INTERFACE_DETAIL_DATA_A*) malloc(required_size); + device_interface_detail_data->cbSize = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA_A); + + /* Get the detailed data for this device. The detail data gives us + the device path for this device, which is then passed into + CreateFile() to get a handle to the device. */ + res = SetupDiGetDeviceInterfaceDetailA(device_info_set, + &device_interface_data, + device_interface_detail_data, + required_size, + NULL, + NULL); + + if (!res) { + /* register_error(dev, "Unable to call SetupDiGetDeviceInterfaceDetail"); + Continue to the next device. */ + goto cont; + } + + /* Make sure this device is of Setup Class "HIDClass" and has a + driver bound to it. */ + for (i = 0; ; i++) { + char driver_name[256]; + + /* Populate devinfo_data. This function will return failure + when there are no more interfaces left. */ + res = SetupDiEnumDeviceInfo(device_info_set, i, &devinfo_data); + if (!res) + goto cont; + + res = SetupDiGetDeviceRegistryPropertyA(device_info_set, &devinfo_data, + SPDRP_CLASS, NULL, (PBYTE)driver_name, sizeof(driver_name), NULL); + if (!res) + goto cont; + + if (strcmp(driver_name, "HIDClass") == 0) { + /* See if there's a driver bound. */ + res = SetupDiGetDeviceRegistryPropertyA(device_info_set, &devinfo_data, + SPDRP_DRIVER, NULL, (PBYTE)driver_name, sizeof(driver_name), NULL); + if (res) + break; + } + } + + //wprintf(L"HandleName: %s\n", device_interface_detail_data->DevicePath); + + /* Open a handle to the device */ + write_handle = open_device(device_interface_detail_data->DevicePath, TRUE); + + /* Check validity of write_handle. */ + if (write_handle == INVALID_HANDLE_VALUE) { + /* Unable to open the device. */ + //register_error(dev, "CreateFile"); + goto cont_close; + } + + + /* Get the Vendor ID and Product ID for this device. */ + attrib.Size = sizeof(HIDD_ATTRIBUTES); + HidD_GetAttributes(write_handle, &attrib); + //wprintf(L"Product/Vendor: %x %x\n", attrib.ProductID, attrib.VendorID); + + /* Check the VID/PID to see if we should add this + device to the enumeration list. */ + if ((vendor_id == 0x0 || attrib.VendorID == vendor_id) && + (product_id == 0x0 || attrib.ProductID == product_id)) { + + #define WSTR_LEN 512 + const char *str; + struct hid_device_info *tmp; + PHIDP_PREPARSED_DATA pp_data = NULL; + HIDP_CAPS caps; + BOOLEAN res; + NTSTATUS nt_res; + wchar_t wstr[WSTR_LEN]; /* TODO: Determine Size */ + size_t len; + + /* VID/PID match. Create the record. */ + tmp = (struct hid_device_info*) calloc(1, sizeof(struct hid_device_info)); + if (cur_dev) { + cur_dev->next = tmp; + } + else { + root = tmp; + } + cur_dev = tmp; + + /* Get the Usage Page and Usage for this device. */ + res = HidD_GetPreparsedData(write_handle, &pp_data); + if (res) { + nt_res = HidP_GetCaps(pp_data, &caps); + if (nt_res == HIDP_STATUS_SUCCESS) { + cur_dev->usage_page = caps.UsagePage; + cur_dev->usage = caps.Usage; + } + + HidD_FreePreparsedData(pp_data); + } + + /* Fill out the record */ + cur_dev->next = NULL; + str = device_interface_detail_data->DevicePath; + if (str) { + len = strlen(str); + cur_dev->path = (char*) calloc(len+1, sizeof(char)); + strncpy(cur_dev->path, str, len+1); + cur_dev->path[len] = '\0'; + } + else + cur_dev->path = NULL; + + /* Serial Number */ + res = HidD_GetSerialNumberString(write_handle, wstr, sizeof(wstr)); + wstr[WSTR_LEN-1] = 0x0000; + if (res) { + cur_dev->serial_number = _wcsdup(wstr); + } + + /* Manufacturer String */ + res = HidD_GetManufacturerString(write_handle, wstr, sizeof(wstr)); + wstr[WSTR_LEN-1] = 0x0000; + if (res) { + cur_dev->manufacturer_string = _wcsdup(wstr); + } + + /* Product String */ + res = HidD_GetProductString(write_handle, wstr, sizeof(wstr)); + wstr[WSTR_LEN-1] = 0x0000; + if (res) { + cur_dev->product_string = _wcsdup(wstr); + } + + /* VID/PID */ + cur_dev->vendor_id = attrib.VendorID; + cur_dev->product_id = attrib.ProductID; + + /* Release Number */ + cur_dev->release_number = attrib.VersionNumber; + + /* Interface Number. It can sometimes be parsed out of the path + on Windows if a device has multiple interfaces. See + http://msdn.microsoft.com/en-us/windows/hardware/gg487473 or + search for "Hardware IDs for HID Devices" at MSDN. If it's not + in the path, it's set to -1. */ + cur_dev->interface_number = -1; + if (cur_dev->path) { + char *interface_component = strstr(cur_dev->path, "&mi_"); + if (interface_component) { + char *hex_str = interface_component + 4; + char *endptr = NULL; + cur_dev->interface_number = strtol(hex_str, &endptr, 16); + if (endptr == hex_str) { + /* The parsing failed. Set interface_number to -1. */ + cur_dev->interface_number = -1; + } + } + } + } + +cont_close: + CloseHandle(write_handle); +cont: + /* We no longer need the detail data. It can be freed */ + free(device_interface_detail_data); + + device_index++; + + } + + /* Close the device information handle. */ + SetupDiDestroyDeviceInfoList(device_info_set); + + return root; + +} + +void HID_API_EXPORT HID_API_CALL hid_free_enumeration(struct hid_device_info *devs) +{ + /* TODO: Merge this with the Linux version. This function is platform-independent. */ + struct hid_device_info *d = devs; + while (d) { + struct hid_device_info *next = d->next; + free(d->path); + free(d->serial_number); + free(d->manufacturer_string); + free(d->product_string); + free(d); + d = next; + } +} + + +HID_API_EXPORT hid_device * HID_API_CALL hid_open(unsigned short vendor_id, unsigned short product_id, const wchar_t *serial_number) +{ + /* TODO: Merge this functions with the Linux version. This function should be platform independent. */ + struct hid_device_info *devs, *cur_dev; + const char *path_to_open = NULL; + hid_device *handle = NULL; + + devs = hid_enumerate(vendor_id, product_id); + cur_dev = devs; + while (cur_dev) { + if (cur_dev->vendor_id == vendor_id && + cur_dev->product_id == product_id) { + if (serial_number) { + if (wcscmp(serial_number, cur_dev->serial_number) == 0) { + path_to_open = cur_dev->path; + break; + } + } + else { + path_to_open = cur_dev->path; + break; + } + } + cur_dev = cur_dev->next; + } + + if (path_to_open) { + /* Open the device */ + handle = hid_open_path(path_to_open); + } + + hid_free_enumeration(devs); + + return handle; +} + +HID_API_EXPORT hid_device * HID_API_CALL hid_open_path(const char *path) +{ + hid_device *dev; + HIDP_CAPS caps; + PHIDP_PREPARSED_DATA pp_data = NULL; + BOOLEAN res; + NTSTATUS nt_res; + + if (hid_init() < 0) { + return NULL; + } + + dev = new_hid_device(); + + /* Open a handle to the device */ + dev->device_handle = open_device(path, FALSE); + + /* Check validity of write_handle. */ + if (dev->device_handle == INVALID_HANDLE_VALUE) { + /* Unable to open the device. */ + register_error(dev, "CreateFile"); + goto err; + } + + /* Set the Input Report buffer size to 64 reports. */ + res = HidD_SetNumInputBuffers(dev->device_handle, 64); + if (!res) { + register_error(dev, "HidD_SetNumInputBuffers"); + goto err; + } + + /* Get the Input Report length for the device. */ + res = HidD_GetPreparsedData(dev->device_handle, &pp_data); + if (!res) { + register_error(dev, "HidD_GetPreparsedData"); + goto err; + } + nt_res = HidP_GetCaps(pp_data, &caps); + if (nt_res != HIDP_STATUS_SUCCESS) { + register_error(dev, "HidP_GetCaps"); + goto err_pp_data; + } + dev->output_report_length = caps.OutputReportByteLength; + dev->input_report_length = caps.InputReportByteLength; + HidD_FreePreparsedData(pp_data); + + dev->read_buf = (char*) malloc(dev->input_report_length); + + return dev; + +err_pp_data: + HidD_FreePreparsedData(pp_data); +err: + free_hid_device(dev); + return NULL; +} + +int HID_API_EXPORT HID_API_CALL hid_write(hid_device *dev, const unsigned char *data, size_t length) +{ + DWORD bytes_written; + BOOL res; + + OVERLAPPED ol; + unsigned char *buf; + memset(&ol, 0, sizeof(ol)); + + /* Make sure the right number of bytes are passed to WriteFile. Windows + expects the number of bytes which are in the _longest_ report (plus + one for the report number) bytes even if the data is a report + which is shorter than that. Windows gives us this value in + caps.OutputReportByteLength. If a user passes in fewer bytes than this, + create a temporary buffer which is the proper size. */ + if (length >= dev->output_report_length) { + /* The user passed the right number of bytes. Use the buffer as-is. */ + buf = (unsigned char *) data; + } else { + /* Create a temporary buffer and copy the user's data + into it, padding the rest with zeros. */ + buf = (unsigned char *) malloc(dev->output_report_length); + memcpy(buf, data, length); + memset(buf + length, 0, dev->output_report_length - length); + length = dev->output_report_length; + } + + res = WriteFile(dev->device_handle, buf, length, NULL, &ol); + + if (!res) { + if (GetLastError() != ERROR_IO_PENDING) { + /* WriteFile() failed. Return error. */ + register_error(dev, "WriteFile"); + bytes_written = -1; + goto end_of_function; + } + } + + /* Wait here until the write is done. This makes + hid_write() synchronous. */ + res = GetOverlappedResult(dev->device_handle, &ol, &bytes_written, TRUE/*wait*/); + if (!res) { + /* The Write operation failed. */ + register_error(dev, "WriteFile"); + bytes_written = -1; + goto end_of_function; + } + +end_of_function: + if (buf != data) + free(buf); + + return bytes_written; +} + + +int HID_API_EXPORT HID_API_CALL hid_read_timeout(hid_device *dev, unsigned char *data, size_t length, int milliseconds) +{ + DWORD bytes_read = 0; + size_t copy_len = 0; + BOOL res; + + /* Copy the handle for convenience. */ + HANDLE ev = dev->ol.hEvent; + + if (!dev->read_pending) { + /* Start an Overlapped I/O read. */ + dev->read_pending = TRUE; + memset(dev->read_buf, 0, dev->input_report_length); + ResetEvent(ev); + res = ReadFile(dev->device_handle, dev->read_buf, dev->input_report_length, &bytes_read, &dev->ol); + + if (!res) { + if (GetLastError() != ERROR_IO_PENDING) { + /* ReadFile() has failed. + Clean up and return error. */ + CancelIo(dev->device_handle); + dev->read_pending = FALSE; + goto end_of_function; + } + } + } + + if (milliseconds >= 0) { + /* See if there is any data yet. */ + res = WaitForSingleObject(ev, milliseconds); + if (res != WAIT_OBJECT_0) { + /* There was no data this time. Return zero bytes available, + but leave the Overlapped I/O running. */ + return 0; + } + } + + /* Either WaitForSingleObject() told us that ReadFile has completed, or + we are in non-blocking mode. Get the number of bytes read. The actual + data has been copied to the data[] array which was passed to ReadFile(). */ + res = GetOverlappedResult(dev->device_handle, &dev->ol, &bytes_read, TRUE/*wait*/); + + /* Set pending back to false, even if GetOverlappedResult() returned error. */ + dev->read_pending = FALSE; + + if (res && bytes_read > 0) { + if (dev->read_buf[0] == 0x0) { + /* If report numbers aren't being used, but Windows sticks a report + number (0x0) on the beginning of the report anyway. To make this + work like the other platforms, and to make it work more like the + HID spec, we'll skip over this byte. */ + bytes_read--; + copy_len = length > bytes_read ? bytes_read : length; + memcpy(data, dev->read_buf+1, copy_len); + } + else { + /* Copy the whole buffer, report number and all. */ + copy_len = length > bytes_read ? bytes_read : length; + memcpy(data, dev->read_buf, copy_len); + } + } + +end_of_function: + if (!res) { + register_error(dev, "GetOverlappedResult"); + return -1; + } + + return copy_len; +} + +int HID_API_EXPORT HID_API_CALL hid_read(hid_device *dev, unsigned char *data, size_t length) +{ + return hid_read_timeout(dev, data, length, (dev->blocking)? -1: 0); +} + +int HID_API_EXPORT HID_API_CALL hid_set_nonblocking(hid_device *dev, int nonblock) +{ + dev->blocking = !nonblock; + return 0; /* Success */ +} + +int HID_API_EXPORT HID_API_CALL hid_send_feature_report(hid_device *dev, const unsigned char *data, size_t length) +{ + BOOL res = HidD_SetFeature(dev->device_handle, (PVOID)data, length); + if (!res) { + register_error(dev, "HidD_SetFeature"); + return -1; + } + + return length; +} + + +int HID_API_EXPORT HID_API_CALL hid_get_feature_report(hid_device *dev, unsigned char *data, size_t length) +{ + BOOL res; +#if 0 + res = HidD_GetFeature(dev->device_handle, data, length); + if (!res) { + register_error(dev, "HidD_GetFeature"); + return -1; + } + return 0; /* HidD_GetFeature() doesn't give us an actual length, unfortunately */ +#else + DWORD bytes_returned; + + OVERLAPPED ol; + memset(&ol, 0, sizeof(ol)); + + res = DeviceIoControl(dev->device_handle, + IOCTL_HID_GET_FEATURE, + data, length, + data, length, + &bytes_returned, &ol); + + if (!res) { + if (GetLastError() != ERROR_IO_PENDING) { + /* DeviceIoControl() failed. Return error. */ + register_error(dev, "Send Feature Report DeviceIoControl"); + return -1; + } + } + + /* Wait here until the write is done. This makes + hid_get_feature_report() synchronous. */ + res = GetOverlappedResult(dev->device_handle, &ol, &bytes_returned, TRUE/*wait*/); + if (!res) { + /* The operation failed. */ + register_error(dev, "Send Feature Report GetOverLappedResult"); + return -1; + } + + /* bytes_returned does not include the first byte which contains the + report ID. The data buffer actually contains one more byte than + bytes_returned. */ + bytes_returned++; + + return bytes_returned; +#endif +} + +int HID_API_EXPORT HID_API_CALL hid_get_descriptor(hid_device *dev, unsigned char *data, size_t length) +{ + BOOL res; + DWORD bytes_returned; + + OVERLAPPED ol; + memset(&ol, 0, sizeof(ol)); + + res = DeviceIoControl(dev->device_handle, + IOCTL_HID_GET_REPORT_DESCRIPTOR, + data, length, + data, length, + &bytes_returned, &ol); + + if (!res) { + if (GetLastError() != ERROR_IO_PENDING) { + /* DeviceIoControl() failed. Return error. */ + register_error(dev, "Get Report Descriptor DeviceIoControl"); + return -1; + } + } + + res = GetOverlappedResult(dev->device_handle, &ol, &bytes_returned, TRUE/*wait*/); + if (!res) { + /* The operation failed. */ + register_error(dev, "Get Report Descriptor GetOverLappedResult"); + return -1; + } + + return bytes_returned; +} + +void HID_API_EXPORT HID_API_CALL hid_close(hid_device *dev) +{ + if (!dev) + return; + CancelIo(dev->device_handle); + free_hid_device(dev); +} + +int HID_API_EXPORT_CALL HID_API_CALL hid_get_manufacturer_string(hid_device *dev, wchar_t *string, size_t maxlen) +{ + BOOL res; + + res = HidD_GetManufacturerString(dev->device_handle, string, sizeof(wchar_t) * MIN(maxlen, MAX_STRING_WCHARS)); + if (!res) { + register_error(dev, "HidD_GetManufacturerString"); + return -1; + } + + return 0; +} + +int HID_API_EXPORT_CALL HID_API_CALL hid_get_product_string(hid_device *dev, wchar_t *string, size_t maxlen) +{ + BOOL res; + + res = HidD_GetProductString(dev->device_handle, string, sizeof(wchar_t) * MIN(maxlen, MAX_STRING_WCHARS)); + if (!res) { + register_error(dev, "HidD_GetProductString"); + return -1; + } + + return 0; +} + +int HID_API_EXPORT_CALL HID_API_CALL hid_get_serial_number_string(hid_device *dev, wchar_t *string, size_t maxlen) +{ + BOOL res; + + res = HidD_GetSerialNumberString(dev->device_handle, string, sizeof(wchar_t) * MIN(maxlen, MAX_STRING_WCHARS)); + if (!res) { + register_error(dev, "HidD_GetSerialNumberString"); + return -1; + } + + return 0; +} + +int HID_API_EXPORT_CALL HID_API_CALL hid_get_indexed_string(hid_device *dev, int string_index, wchar_t *string, size_t maxlen) +{ + BOOL res; + + res = HidD_GetIndexedString(dev->device_handle, string_index, string, sizeof(wchar_t) * MIN(maxlen, MAX_STRING_WCHARS)); + if (!res) { + register_error(dev, "HidD_GetIndexedString"); + return -1; + } + + return 0; +} + + +HID_API_EXPORT const wchar_t * HID_API_CALL hid_error(hid_device *dev) +{ + return (wchar_t*)dev->last_error_str; +} + + +/*#define PICPGM*/ +/*#define S11*/ +#define P32 +#ifdef S11 + unsigned short VendorID = 0xa0a0; + unsigned short ProductID = 0x0001; +#endif + +#ifdef P32 + unsigned short VendorID = 0x04d8; + unsigned short ProductID = 0x3f; +#endif + + +#ifdef PICPGM + unsigned short VendorID = 0x04d8; + unsigned short ProductID = 0x0033; +#endif + + +#if 0 +int __cdecl main(int argc, char* argv[]) +{ + int res; + unsigned char buf[65]; + + UNREFERENCED_PARAMETER(argc); + UNREFERENCED_PARAMETER(argv); + + /* Set up the command buffer. */ + memset(buf,0x00,sizeof(buf)); + buf[0] = 0; + buf[1] = 0x81; + + + /* Open the device. */ + int handle = open(VendorID, ProductID, L"12345"); + if (handle < 0) + printf("unable to open device\n"); + + + /* Toggle LED (cmd 0x80) */ + buf[1] = 0x80; + res = write(handle, buf, 65); + if (res < 0) + printf("Unable to write()\n"); + + /* Request state (cmd 0x81) */ + buf[1] = 0x81; + write(handle, buf, 65); + if (res < 0) + printf("Unable to write() (2)\n"); + + /* Read requested state */ + read(handle, buf, 65); + if (res < 0) + printf("Unable to read()\n"); + + /* Print out the returned buffer. */ + for (int i = 0; i < 4; i++) + printf("buf[%d]: %d\n", i, buf[i]); + + return 0; +} +#endif + +#ifdef __cplusplus +} /* extern "C" */ +#endif diff -Nru flightgear-2017.3.1+dfsg/CMakeLists.txt flightgear-2018.1.1+dfsg/CMakeLists.txt --- flightgear-2017.3.1+dfsg/CMakeLists.txt 2017-09-20 07:51:30.000000000 +0000 +++ flightgear-2018.1.1+dfsg/CMakeLists.txt 2018-04-06 19:43:33.000000000 +0000 @@ -48,6 +48,7 @@ # We have some custom .cmake scripts not in the official distribution. set(CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/CMakeModules;${CMAKE_MODULE_PATH}") +set_property(GLOBAL PROPERTY FIND_LIBRARY_USE_LIB64_PATHS TRUE) # Warning when build is not an out-of-source build. string(COMPARE EQUAL "${CMAKE_SOURCE_DIR}" "${CMAKE_BINARY_DIR}" InSourceBuild) @@ -120,6 +121,12 @@ list(APPEND PLATFORM_LIBS ${COCOA_LIBRARY} ${CORESERVICES_LIBRARY}) elseif(WIN32) list(APPEND PLATFORM_LIBS "Shlwapi.lib") + + set(CMAKE_INSTALL_SYSTEM_RUNTIME_DESTINATION "bin") + set(CMAKE_INSTALL_UCRT_LIBRARIES TRUE) + include(InstallRequiredSystemLibraries) + MESSAGE("Installing: ${CMAKE_INSTALL_SYSTEM_RUNTIME_LIBS}") + elseif(${CMAKE_SYSTEM_NAME} MATCHES "Linux" OR ${CMAKE_SYSTEM_NAME} MATCHES "FreeBSD") find_package(Threads REQUIRED) @@ -200,6 +207,7 @@ option(ENABLE_TRAFFIC "Set to ON to build the external traffic generator modules" ON) option(ENABLE_FGQCANVAS "Set to ON to build the Qt-based remote canvas application" OFF) option(ENABLE_DEMCONVERT "Set to ON to build the dem conversion tool (default)" ON) +option(ENABLE_HID_INPUT "Set to ON to build HID-based input code (default)" OFF) include (DetectArch) @@ -232,6 +240,8 @@ if(EVENT_INPUT) if(APPLE) add_definitions(-DWITH_EVENTINPUT) + find_library(IOKIT_FRAMEWORK IOKit) + list(APPEND EVENT_INPUT_LIBRARIES ${IOKIT_FRAMEWORK}) elseif(CMAKE_SYSTEM_NAME MATCHES "Linux|FreeBSD") if(NOT UDEV_FOUND) message(WARNING "UDev not found, event input is disabled!") @@ -245,6 +255,10 @@ message(WARNING "event-based input is not supported on this platform yet") endif() + if (ENABLE_HID_INPUT) + list(APPEND EVENT_INPUT_LIBRARIES hidapi) + endif() + # Keep PLIB INPUT enabled as long as EventInput does not replace current joystick configurations. set(ENABLE_PLIB_JOYSTICK 1) else(EVENT_INPUT) @@ -308,22 +322,61 @@ endif (USE_DBUS) ############################################################################## + +function(check_private_headers_exist module private_includes_var) +message(STATUS "Checking whether private include directories for module ${module} exist") +foreach(_dir ${private_includes_var}) + if(NOT EXISTS "${_dir}") + message(FATAL_ERROR "The private include directory ${_dir} for module ${module} do not exist! Please make sure your Qt5 installation contains private headers.\nThe required directories:\n ${private_includes_var}") + endif() +endforeach() +endfunction() +function(check_private_header_exists _file _dirs) +set(_found FALSE) +foreach(_dir ${_dirs}) + if(EXISTS "${_dir}/${_file}") + set(_found TRUE) + endif() +endforeach() +if(NOT _found) + message(FATAL_ERROR "The private include file ${_file} was not found in directory ${_dirs}! Please make sure your Qt5 installation contains private headers.") +endif() +endfunction() + +############################################################################## ## Qt5 setup setup if (ENABLE_QT) - message(STATUS "Qt launcher enabled, checking for Qt 5.1 / qmake") - find_package(Qt5 5.1 COMPONENTS Widgets Network Qml) - if (Qt5Widgets_FOUND) - message(STATUS "Will enable Qt launcher GUI") - message(STATUS " Qt5Widgets version: ${Qt5Widgets_VERSION_STRING}") - message(STATUS " Qt5Widgets include dir: ${Qt5Widgets_INCLUDE_DIRS}") - set(HAVE_QT 1) - else() - # don't try to build FGQCanvas if Qt wasn't found correctly - set(ENABLE_FGQCANVAS OFF) - endif() + message(STATUS "Qt launcher enabled, checking for Qt 5.4 / qmake") + find_package(Qt5 5.4 COMPONENTS Widgets Network Qml Quick QuickWidgets) + if (Qt5Widgets_FOUND) + message(STATUS "Will enable Qt launcher GUI") + message(STATUS " Qt5Widgets version: ${Qt5Widgets_VERSION_STRING}") + message(STATUS " Qt5Widgets include dir: ${Qt5Widgets_INCLUDE_DIRS}") + + # copied from KDAB's GammaRay CMakeLists.txt + # Sanity checking, we need private includes for the following modules + + check_private_headers_exist("Qt5Gui" "${Qt5Gui_PRIVATE_INCLUDE_DIRS}") + + #HACK: CMake with broken Qt5Quick_PRIVATE_INCLUDE_DIRS + if (NOT "${Qt5Quick_PRIVATE_INCLUDE_DIRS}" MATCHES "/QtQuick/") + string(REPLACE "/QtCore" "/QtQuick" replaceme "${Qt5Core_PRIVATE_INCLUDE_DIRS}") + list(APPEND Qt5Quick_PRIVATE_INCLUDE_DIRS ${Qt5Qml_PRIVATE_INCLUDE_DIRS}) + list(APPEND Qt5Quick_PRIVATE_INCLUDE_DIRS ${replaceme}) + list(REMOVE_DUPLICATES Qt5Quick_PRIVATE_INCLUDE_DIRS) + endif() + check_private_header_exists("private/qquickitem_p.h" "${Qt5Quick_PRIVATE_INCLUDE_DIRS}") + + message(STATUS " Qt5Quick private include dir: ${Qt5Quick_PRIVATE_INCLUDE_DIRS}") + + set(HAVE_QT 1) + else() + # don't try to build FGQCanvas if Qt wasn't found correctly + set(ENABLE_FGQCANVAS OFF) + endif() else() - message(STATUS "Qt support disabled") - set(ENABLE_FGQCANVAS OFF) + message(STATUS "Qt support disabled") + set(ENABLE_FGQCANVAS OFF) endif (ENABLE_QT) ############################################################################## diff -Nru flightgear-2017.3.1+dfsg/CMakeModules/FindAAX.cmake flightgear-2018.1.1+dfsg/CMakeModules/FindAAX.cmake --- flightgear-2017.3.1+dfsg/CMakeModules/FindAAX.cmake 2017-09-20 07:51:30.000000000 +0000 +++ flightgear-2018.1.1+dfsg/CMakeModules/FindAAX.cmake 2018-04-06 19:43:33.000000000 +0000 @@ -10,12 +10,13 @@ # # Created by Erik Hofman. -FIND_PATH(AAX_INCLUDE_DIR aax/aeonwave.hpp +FIND_PATH(AAX_INCLUDE_DIR aax/aax.h HINTS $ENV{AAXDIR} $ENV{ProgramFiles}/aax $ENV{ProgramFiles}/AeonWave $ENV{ProgramFiles}/Adalin/AeonWave + ${CMAKE_SOURCE_DIR}/aax PATH_SUFFIXES include PATHS ~/Library/Frameworks @@ -26,23 +27,35 @@ ) FIND_LIBRARY(AAX_LIBRARY - NAMES AAX aax AAX32 libAAX32 + NAMES AAX aax AAX32 HINTS $ENV{AAXDIR} $ENV{ProgramFiles}/AAX $ENV{ProgramFiles}/AeonWave $ENV{ProgramFiles}/Adalin/AeonWave + ${CMAKE_BUILD_DIR}/aax PATH_SUFFIXES bin lib lib/${CMAKE_LIBRARY_ARCHITECTURE} lib64 libs64 libs libs/Win32 libs/Win64 PATHS ~/Library/Frameworks /Library/Frameworks + /usr/local /usr /opt - /usr/local ) -SET(AAX_FOUND "NO") IF(AAX_LIBRARY AND AAX_INCLUDE_DIR) SET(AAX_FOUND "YES") +ELSE(AAX_LIBRARY AND AAX_INCLUDE_DIR) + IF(NOT AAX_INCLUDE_DIR) + MESSAGE(FATAL_ERROR "Unable to find the AAX library development files.") + SET(AAX_FOUND "NO") + ENDIF(NOT AAX_INCLUDE_DIR) + IF(NOT AAX_LIBRARY) + IF(SINGLE_PACKAGE) + SET(AAX_LIBRARY "${aax_BUILD_DIR}/aax/AAX32.dll") + SET(AAX_FOUND "YES") + ELSE(SINGLE_PACKAGE) + ENDIF(SINGLE_PACKAGE) + ENDIF(NOT AAX_LIBRARY) ENDIF(AAX_LIBRARY AND AAX_INCLUDE_DIR) diff -Nru flightgear-2017.3.1+dfsg/debian/changelog flightgear-2018.1.1+dfsg/debian/changelog --- flightgear-2017.3.1+dfsg/debian/changelog 2018-03-13 20:21:34.000000000 +0000 +++ flightgear-2018.1.1+dfsg/debian/changelog 2018-04-23 13:19:15.000000000 +0000 @@ -1,3 +1,18 @@ +flightgear (1:2018.1.1+dfsg-1) unstable; urgency=medium + + * New upstream version 2018.1.1+dfsg + - Refresh patches + - Update versioned Build-Dependencies to new data packages + - Update d/copyright for new code in 3rdparty + - Add Build-Depends on qtbase5-dev, qtbase5-private-dev, + and qtdeclarative5-private-dev + * Update Standards-Version to 4.1.4 + - Use HTTPS for d/copyright (really this time) + - Remove target get-orig-source and use uscan instead + * Add myself as uploader + + -- Dr. Tobias Quathamer Mon, 23 Apr 2018 15:19:15 +0200 + flightgear (1:2017.3.1+dfsg-4) unstable; urgency=medium * Team upload. diff -Nru flightgear-2017.3.1+dfsg/debian/control flightgear-2018.1.1+dfsg/debian/control --- flightgear-2017.3.1+dfsg/debian/control 2018-03-13 20:20:56.000000000 +0000 +++ flightgear-2018.1.1+dfsg/debian/control 2018-04-23 13:17:13.000000000 +0000 @@ -2,11 +2,13 @@ Section: games Priority: optional Maintainer: Debian FlightGear Crew -Uploaders: Ove Kaaven , Markus Wanner +Uploaders: Ove Kaaven , + Markus Wanner , + Dr. Tobias Quathamer Build-Depends: cmake, debhelper (>= 11~), - flightgear-data-ai (>= 1:2017.3.1~), - flightgear-data-base (>= 1:2017.3.1~), + flightgear-data-ai (>= 1:2018.1.1~), + flightgear-data-base (>= 1:2018.1.1~), flite1-dev, libboost-dev, libcurl4-gnutls-dev, @@ -20,8 +22,8 @@ libopenscenegraph-3.4-dev [!armel !armhf], libopenscenegraph-dev (>= 3.2.0~) [armel armhf], libplib-dev, - libsimgear-dev (<= 1:2017.3.999), - libsimgear-dev (>> 1:2017.3.0~), + libsimgear-dev (<= 1:2018.1.999), + libsimgear-dev (>> 1:2018.1.1~), libspeex-dev, libspeexdsp-dev, libsqlite3-dev, @@ -29,16 +31,19 @@ libudns-dev, libusbhid-dev [kfreebsd-any], libx11-dev, + qtbase5-dev, + qtbase5-private-dev, qtdeclarative5-dev, + qtdeclarative5-private-dev, zlib1g-dev -Standards-Version: 4.1.3 +Standards-Version: 4.1.4 Homepage: http://www.flightgear.org/ Vcs-Browser: https://salsa.debian.org/debian/flightgear Vcs-Git: https://salsa.debian.org/debian/flightgear.git Package: flightgear Architecture: any -Depends: flightgear-data-all (>= 1:2017.3.1~), +Depends: flightgear-data-all (>= 1:2018.1.1~), ${misc:Depends}, ${shlibs:Depends} Conflicts: metar diff -Nru flightgear-2017.3.1+dfsg/debian/copyright flightgear-2018.1.1+dfsg/debian/copyright --- flightgear-2017.3.1+dfsg/debian/copyright 2018-03-13 15:36:24.000000000 +0000 +++ flightgear-2018.1.1+dfsg/debian/copyright 2018-04-23 13:10:20.000000000 +0000 @@ -234,6 +234,10 @@ Copyright: 2009 Dave Gamble License: MIT +Files: 3rdparty/hidapi/* +Copyright: 2009-2010 Alan Ott, Signal 11 Software +License: GPL-3+ + Files: debian/* Copyright: 1997-1999 Joey Hess 2001-2012 Ove Kaaven @@ -255,6 +259,23 @@ A copy of the license is available on Debian systems at /usr/share/common-licenses/GPL-2 +License: GPL-3+ + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + . + This package is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + . + You should have received a copy of the GNU General Public License + along with this program. If not, see . + . + On Debian systems, the complete text of the GNU General + Public License version 3 can be found in "/usr/share/common-licenses/GPL-3". + License: BSD-3-clause Redistributions of source code must retain the above copy‐ right notice, this list of conditions and the following dis‐ diff -Nru flightgear-2017.3.1+dfsg/debian/copyright.auto flightgear-2018.1.1+dfsg/debian/copyright.auto --- flightgear-2017.3.1+dfsg/debian/copyright.auto 2018-03-13 10:29:00.000000000 +0000 +++ flightgear-2018.1.1+dfsg/debian/copyright.auto 2018-04-22 13:19:44.000000000 +0000 @@ -1,4 +1,4 @@ -Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Upstream-Name: flightgear Upstream-Contact: Curtis L. Olson Source: http://www.flightgear.org @@ -608,4 +608,3 @@ License: public-domain Comment: Add the corresponding license text here - diff -Nru flightgear-2017.3.1+dfsg/debian/patches/spelling_20160206.patch flightgear-2018.1.1+dfsg/debian/patches/spelling_20160206.patch --- flightgear-2017.3.1+dfsg/debian/patches/spelling_20160206.patch 2018-03-13 10:29:00.000000000 +0000 +++ flightgear-2018.1.1+dfsg/debian/patches/spelling_20160206.patch 2018-04-22 13:26:57.000000000 +0000 @@ -6,7 +6,7 @@ --- a/src/GUI/AddCatalogDialog.cxx +++ b/src/GUI/AddCatalogDialog.cxx -@@ -126,7 +126,7 @@ +@@ -153,7 +153,7 @@ break; default: @@ -15,15 +15,15 @@ } ui->resultsSummaryLabel->setText(s); -@@ -165,7 +165,7 @@ +@@ -192,7 +192,7 @@ + void AddCatalogDialog::reject() { - if (m_result && !m_result->id().empty()) { -- // user may have successfully download the catalog, but choosen -+ // user may have successfully download the catalog, but chosen - // not to add it. so remove it here - m_packageRoot->removeCatalogById(m_result->id()); - } +- // user may have successfully download the catalog, but choosen ++ // user may have successfully downloaded the catalog, but chosen + // not to add it. so remove it here + + // however, if we got in here as part of re-validating an existing --- a/src/GUI/SetupRootDialog.cxx +++ b/src/GUI/SetupRootDialog.cxx @@ -243,13 +243,13 @@ @@ -80,7 +80,7 @@ --- a/src/Navaids/routePath.cxx +++ b/src/Navaids/routePath.cxx -@@ -748,7 +748,7 @@ +@@ -750,7 +750,7 @@ } else { // climb @@ -89,7 +89,7 @@ if (prev < 0) { return 0.0; } -@@ -764,7 +764,7 @@ +@@ -766,7 +766,7 @@ } @@ -98,7 +98,7 @@ { const WayptData& w(waypoints[index]); if (w.wpt->altitudeRestriction() == RESTRICT_AT) { -@@ -778,12 +778,12 @@ +@@ -780,12 +780,12 @@ } if (index == 0) { @@ -225,7 +225,7 @@ } --- a/src/Scripting/NasalPositioned.cxx +++ b/src/Scripting/NasalPositioned.cxx -@@ -1693,7 +1693,7 @@ +@@ -1825,7 +1825,7 @@ argOffset += geodFromArgs(args, 0, argc, pos); if (!naIsNum(args[argOffset])) { @@ -234,7 +234,7 @@ } FGPositioned::Type type = FGPositioned::INVALID; -@@ -1718,7 +1718,7 @@ +@@ -1853,7 +1853,7 @@ argOffset += geodFromArgs(args, 0, argc, pos); if (!naIsNum(args[argOffset])) { @@ -243,7 +243,7 @@ } FGPositioned::Type type = FGPositioned::INVALID; -@@ -1746,7 +1746,7 @@ +@@ -1882,7 +1882,7 @@ argOffset += geodFromArgs(args, 0, argc, pos); if (!naIsString(args[argOffset])) { @@ -252,7 +252,7 @@ } FGPositioned::Type type = FGPositioned::INVALID; -@@ -1773,7 +1773,7 @@ +@@ -1909,7 +1909,7 @@ argOffset += geodFromArgs(args, 0, argc, pos); if (!naIsString(args[argOffset])) { @@ -1011,7 +1011,7 @@ else --- a/src/FDM/YASim/Rotor.cpp +++ b/src/FDM/YASim/Rotor.cpp -@@ -140,7 +140,7 @@ +@@ -139,7 +139,7 @@ _balance2=1; _properties_tied=0; _num_ground_contact_pos=0; @@ -1020,7 +1020,7 @@ _tilt_yaw=0; _tilt_roll=0; _tilt_pitch=0; -@@ -751,21 +751,21 @@ +@@ -750,21 +750,21 @@ { lval = Math::clamp(lval, -1, 1); _tilt_yaw = _min_tilt_yaw+(lval+1)/2*(_max_tilt_yaw-_min_tilt_yaw); @@ -1045,7 +1045,7 @@ } void Rotor::setCollective(float lval) -@@ -1047,7 +1047,7 @@ +@@ -1046,7 +1046,7 @@ void Rotor::updateDirectionsAndPositions(float *rot) { @@ -1054,7 +1054,7 @@ { rot[0]=rot[1]=rot[2]=0; return; -@@ -1150,7 +1150,7 @@ +@@ -1149,7 +1149,7 @@ #undef _base #undef _forward #undef _normal @@ -1065,7 +1065,7 @@ void Rotor::compile() --- a/src/FDM/YASim/Rotor.hpp +++ b/src/FDM/YASim/Rotor.hpp -@@ -163,7 +163,7 @@ +@@ -162,7 +162,7 @@ float _ground_contact_pos[16][3]; int _num_ground_contact_pos; float _ground_effect_altitude; @@ -1074,7 +1074,7 @@ float _normal[3];//the normal vector (direction of rotormast, pointing up) float _normal_with_yaw_roll[3];//the normal vector (perpendicular to rotordisc) float _forward[3]; -@@ -241,7 +241,7 @@ +@@ -240,7 +240,7 @@ bool _shared_flap_hinge; float _grav_direction[3]; int _properties_tied; diff -Nru flightgear-2017.3.1+dfsg/debian/patches/spelling_20160529.patch flightgear-2018.1.1+dfsg/debian/patches/spelling_20160529.patch --- flightgear-2017.3.1+dfsg/debian/patches/spelling_20160529.patch 2018-03-13 10:29:00.000000000 +0000 +++ flightgear-2018.1.1+dfsg/debian/patches/spelling_20160529.patch 2018-04-22 13:28:01.000000000 +0000 @@ -6,7 +6,7 @@ --- a/src/Main/positioninit.cxx +++ b/src/Main/positioninit.cxx -@@ -438,7 +438,7 @@ +@@ -431,7 +431,7 @@ if( navlist.size() > 1 ) { std::ostringstream buf; diff -Nru flightgear-2017.3.1+dfsg/debian/patches/spelling_20160920.patch flightgear-2018.1.1+dfsg/debian/patches/spelling_20160920.patch --- flightgear-2017.3.1+dfsg/debian/patches/spelling_20160920.patch 2018-03-13 10:29:00.000000000 +0000 +++ flightgear-2018.1.1+dfsg/debian/patches/spelling_20160920.patch 2018-04-22 13:28:08.000000000 +0000 @@ -6,7 +6,7 @@ --- a/src/GUI/InstallSceneryDialog.cxx +++ b/src/GUI/InstallSceneryDialog.cxx -@@ -257,7 +257,7 @@ +@@ -276,7 +276,7 @@ void InstallSceneryDialog::onExtractError(QString file, QString msg) { @@ -50,7 +50,7 @@ SGSubsystem* createSubsystemByName(const std::string& name); --- a/src/Scripting/NasalSys.cxx +++ b/src/Scripting/NasalSys.cxx -@@ -1474,7 +1474,7 @@ +@@ -1501,7 +1501,7 @@ { NasalCommandDict::iterator it = _commands.find(name); if (it == _commands.end()) { @@ -61,7 +61,7 @@ --- a/src/GUI/LauncherMainWindow.cxx +++ b/src/GUI/LauncherMainWindow.cxx -@@ -133,7 +133,7 @@ +@@ -121,7 +121,7 @@ m_subsystemIdleTimer = new QTimer(this); m_subsystemIdleTimer->setInterval(0); connect(m_subsystemIdleTimer, &QTimer::timeout, @@ -69,9 +69,9 @@ + this, &LauncherMainWindow::onSubsystemIdleTimeout); m_subsystemIdleTimer->start(); - // create and configure the proxy model -@@ -951,7 +951,7 @@ - maybeRestoreAircraftSelection(); + connect(m_ui->flyButton, SIGNAL(clicked()), this, SLOT(onRun())); +@@ -794,7 +794,7 @@ + m_ui->settingsDescription->setText(s); } -void LauncherMainWindow::onSubsytemIdleTimeout() @@ -81,7 +81,7 @@ } --- a/src/GUI/LauncherMainWindow.hxx +++ b/src/GUI/LauncherMainWindow.hxx -@@ -89,7 +89,7 @@ +@@ -123,7 +123,7 @@ void updateSettingsSummary(); diff -Nru flightgear-2017.3.1+dfsg/debian/patches/spelling_20170725.patch flightgear-2018.1.1+dfsg/debian/patches/spelling_20170725.patch --- flightgear-2017.3.1+dfsg/debian/patches/spelling_20170725.patch 2018-03-13 10:29:00.000000000 +0000 +++ flightgear-2018.1.1+dfsg/debian/patches/spelling_20170725.patch 2018-04-22 13:28:18.000000000 +0000 @@ -6,7 +6,7 @@ --- a/src/Airports/airport.hxx +++ b/src/Airports/airport.hxx -@@ -211,7 +211,7 @@ +@@ -209,7 +209,7 @@ /** * Filter which passes specified port type and in case of airport checks @@ -59,7 +59,7 @@ bool isInitialized(void) { return _initialized;} inline std::string& get_sound_path() { return _fxpath;} -@@ -289,7 +289,7 @@ +@@ -284,7 +284,7 @@ pitch*speed ); _fx->set_velocity( velocity ); } @@ -114,7 +114,7 @@ Get rid of a multiple defined symbol warning" src/FDM/LaRCsim/ls_step.c --- a/utils/fgqcanvas/fgcanvaspath.cpp +++ b/utils/fgqcanvas/fgcanvaspath.cpp -@@ -322,17 +322,17 @@ +@@ -588,17 +588,17 @@ QByteArrayList result; size_t pos = 0; std::string strData(d.data()); diff -Nru flightgear-2017.3.1+dfsg/debian/rules flightgear-2018.1.1+dfsg/debian/rules --- flightgear-2017.3.1+dfsg/debian/rules 2018-03-13 10:29:00.000000000 +0000 +++ flightgear-2018.1.1+dfsg/debian/rules 2018-04-22 13:29:56.000000000 +0000 @@ -61,6 +61,3 @@ copyright_check: ${MAKE} -f /usr/share/cdbs/1/rules/utils.mk debian/stamp-copyright-check rm debian/stamp-copyright-check - -get-orig-source: - uscan --download-current-version --verbose --rename diff -Nru flightgear-2017.3.1+dfsg/scripts/python/TerraSync/terrasync/exceptions.py flightgear-2018.1.1+dfsg/scripts/python/TerraSync/terrasync/exceptions.py --- flightgear-2017.3.1+dfsg/scripts/python/TerraSync/terrasync/exceptions.py 1970-01-01 00:00:00.000000000 +0000 +++ flightgear-2018.1.1+dfsg/scripts/python/TerraSync/terrasync/exceptions.py 2018-04-06 19:43:33.000000000 +0000 @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- + +# exceptions.py --- Custom exception classes for terrasync.py +# +# Copyright (C) 2018 Florent Rougon +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; either version 2 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +# Generic exception class for terrasync.py, to be subclassed for each specific +# kind of exception. +class TerraSyncPyException(Exception): + def __init__(self, message=None, *, mayCapitalizeMsg=True): + """Initialize a TerraSyncPyException instance. + + Except in cases where 'message' starts with a proper noun or + something like that, its first character should be given in + lower case. Automated treatments of this exception may print the + message with its first character changed to upper case, unless + 'mayCapitalizeMsg' is False. In other words, if the case of the + first character of 'message' must not be changed under any + circumstances, set 'mayCapitalizeMsg' to False. + + """ + self.message = message + self.mayCapitalizeMsg = mayCapitalizeMsg + + def __str__(self): + return self.completeMessage() + + def __repr__(self): + return "{}.{}({!r})".format(__name__, type(self).__name__, self.message) + + # Typically overridden by subclasses with a custom constructor + def detail(self): + return self.message + + def completeMessage(self): + if self.message: + return "{shortDesc}: {detail}".format( + shortDesc=self.ExceptionShortDescription, + detail=self.detail()) + else: + return self.ExceptionShortDescription + + ExceptionShortDescription = "terrasync.py generic exception" + + +class UserError(TerraSyncPyException): + """Exception raised when the program is used in an incorrect way.""" + ExceptionShortDescription = "User error" + +class NetworkError(TerraSyncPyException): + """Exception raised when getting a network error even after retrying.""" + ExceptionShortDescription = "Network error" + +class RepoDataError(TerraSyncPyException): + """ + Exception raised when getting invalid data from the TerraSync repository.""" + ExceptionShortDescription = "Invalid data from the TerraSync repository" + +class InvalidDirIndexFile(RepoDataError): + """Exception raised when getting invalid data from a .dirindex file.""" + ExceptionShortDescription = "Invalid .dirindex file" diff -Nru flightgear-2017.3.1+dfsg/scripts/python/TerraSync/terrasync/main.py flightgear-2018.1.1+dfsg/scripts/python/TerraSync/terrasync/main.py --- flightgear-2017.3.1+dfsg/scripts/python/TerraSync/terrasync/main.py 1970-01-01 00:00:00.000000000 +0000 +++ flightgear-2018.1.1+dfsg/scripts/python/TerraSync/terrasync/main.py 2018-04-06 19:43:33.000000000 +0000 @@ -0,0 +1,753 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# main.py --- Main module for terrasync.py +# +# Copyright (C) 2016 Torsten Dreyer +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; either version 2 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +import argparse +import enum +import hashlib +import os +import re +import shutil +import sys +import time +import urllib + +from urllib.parse import urlparse, urljoin +from http.client import HTTPConnection, _CS_IDLE, HTTPException +from os import listdir +from os.path import isfile, isdir, join + +from .exceptions import UserError, NetworkError, RepoDataError, \ + InvalidDirIndexFile +from .virtual_path import VirtualPath + + +PROGNAME = os.path.basename(sys.argv[0]) + +class ExitStatus(enum.Enum): + SUCCESS = 0 + # The program exit status is 1 when an exception isn't caught. + ERROR = 1 + CHECK_MODE_FOUND_MISMATCH = 2 + + +# ***************************************************************************** +# * Utility functions * +# ***************************************************************************** + +# If a path matches this regexp, we really don't want to delete it recursively +# (“cre” stands for “compiled regexp”). +_removeDirectoryTree_dangerous_cre = re.compile( + r"""^(/ (home (/ [^/]*)? )? /* | # for Unix-like systems + [a-zA-Z]: [\/]* # for Windows + )$""", re.VERBOSE) + +def removeDirectoryTree(base, whatToRemove): + """Recursively remove directory 'whatToRemove', with safety checks. + + This function ensures that 'whatToRemove' does not resolve to a + directory such as /, /home, /home/foobar, C:\, d:\, etc. It is also + an error if 'whatToRemove' does not literally start with the value + of 'base' (IOW, this function refuses to erase anything that is not + under 'base'). + + 'whatToRemove' is *not* interpreted relatively to 'base' (this would + be doable, just a different API). + + """ + assert os.path.isdir(base), "Not a directory: {!r}".format(base) + assert (base and + whatToRemove.startswith(base) and + whatToRemove[len(base):].startswith(os.sep)), \ + "Unexpected base path for removeDirectoryTree(): {!r}".format(base) + absPath = os.path.abspath(whatToRemove) + + if _removeDirectoryTree_dangerous_cre.match(absPath): + raise UserError("in order to protect your data, refusing to " + "recursively delete '{}'".format(absPath)) + else: + shutil.rmtree(absPath) + + +def computeHash(fileLike): + hash = hashlib.sha1() + + for chunk in iter(lambda: fileLike.read(4096), b""): + hash.update(chunk) + + return hash.hexdigest() + + +def hashForFile(fname): + with open(fname, "rb") as f: + return computeHash(f) + + +# ***************************************************************************** +# * Network-related classes * +# ***************************************************************************** + +class HTTPGetCallback: + def __init__(self, src, callback): + """Initialize an HTTPGetCallback instance. + + src -- a VirtualPath instance (corresponding to the path on + the server for which a GET request is to be issued) + callback -- a function taking two parameters: the URL (string) + and an http.client.HTTPResponse instance. When + invoked, the callback return value will be returned + by HTTPGetter.get(). + + """ + self.callback = callback + self.src = src + +class HTTPGetter: + def __init__(self, baseUrl, maxPending=10): + self.baseUrl = baseUrl + self.parsedBaseUrl = urlparse(baseUrl) + self.maxPending = maxPending + self.requests = [] + self.pendingRequests = [] + self.httpConnection = HTTPConnection(self.parsedBaseUrl.netloc) + self.httpRequestHeaders = headers = {'Host':self.parsedBaseUrl.netloc,'Content-Length':0,'Connection':'Keep-Alive','User-Agent':'FlightGear terrasync.py'} + + def assemblePath(self, httpGetCallback): + """Return the path-on-server for the file to download. + + Example: '/scenery/Airports/N/E/4/.dirindex' + + """ + assert not self.parsedBaseUrl.path.endswith('/'), \ + repr(self.parsedBaseUrl) + return self.parsedBaseUrl.path + str(httpGetCallback.src) + + def assembleUrl(self, httpGetCallback): + """Return the URL of the file to download.""" + baseUrl = self.parsedBaseUrl.geturl() + assert not baseUrl.endswith('/'), repr(baseUrl) + + return urljoin(baseUrl + '/', httpGetCallback.src.asRelative()) + + def doGet(self, httpGetCallback): + conn = self.httpConnection + pathOnServer = self.assemblePath(httpGetCallback) + self.httpConnection.request("GET", pathOnServer, None, + self.httpRequestHeaders) + httpResponse = self.httpConnection.getresponse() + + # 'httpResponse' is an http.client.HTTPResponse instance + return httpGetCallback.callback(self.assembleUrl(httpGetCallback), + httpResponse) + + def get(self, httpGetCallback): + nbRetries = nbRetriesLeft = 5 + + while True: + try: + return self.doGet(httpGetCallback) + except HTTPException as exc: + if nbRetriesLeft == 0: + raise NetworkError( + "after {nbRetries} retries for URL {url}: {errMsg}" + .format(nbRetries=nbRetries, + url=self.assembleUrl(httpGetCallback), + errMsg=exc)) from exc + + # Try to reconnect + self.httpConnection.close() + time.sleep(1) + self.httpConnection.connect() + nbRetriesLeft -= 1 + + +################################################################################################################################# +class DirIndex: + + def __init__(self, dirIndexFile): + self.d = [] + self.f = [] + self.version = 0 + self.path = None # will be a VirtualPath instance when set + + # readFrom() stores the raw contents of the .dirindex file in this + # attribute. This is useful for troubleshooting. + self._rawContents = None + + with open(dirIndexFile) as f: + self.readFrom(f) + + self._sanityCheck() + + def readFrom(self, readable): + self._rawContents = readable.read() + + for line in self._rawContents.split('\n'): + line = line.strip() + if line.startswith('#'): + continue + + tokens = line.split(':') + if len(tokens) == 0: + continue + + if tokens[0] == "version": + self.version = int(tokens[1]) + + elif tokens[0] == "path": + # This is relative to the repository root + self.path = VirtualPath(tokens[1]) + + elif tokens[0] == "d": + self.d.append({ 'name': tokens[1], 'hash': tokens[2] }) + + elif tokens[0] == "f": + self.f.append({ 'name': tokens[1], 'hash': tokens[2], 'size': tokens[3] }) + + def _sanityCheck(self): + if self.path is None: + assert self._rawContents is not None + + firstLines = self._rawContents.split('\n')[:5] + raise InvalidDirIndexFile( + "no 'path' field found; the first lines of this .dirindex file " + "follow:\n\n" + '\n'.join(firstLines)) + + def getVersion(self): + return self.version + + def getPath(self): + return self.path + + def getDirectories(self): + return self.d + + def getFiles(self): + return self.f + +################################################################################################################################# +class HTTPDownloadRequest(HTTPGetCallback): + def __init__(self, terrasync, src, dst, callback = None ): + """Initialize an HTTPDownloadRequest instance. + + terrasync -- a TerraSync instance + src -- a VirtualPath instance (corresponding to the path + on the server for which a GET request is to be + issued) + dst -- file path (or whatever open() accepts) where the + downloaded data is to be stored + callback -- a function that will be called if the download is + successful, or None if no such callback is desired. + The function must take one parameter: when invoked, + it will be passed this HTTPDownloadRequest + instance. Its return value is ignored. + + """ + super().__init__(src, self.callback) + self.terrasync = terrasync + self.dst = dst + self.mycallback = callback + + # 'httpResponse' is an http.client.HTTPResponse instance + def callback(self, url, httpResponse): + # I suspect this doesn't handle HTTP redirects and things like that. As + # mentioned at , + # http.client is a low-level interface that should normally not be used + # directly! + if httpResponse.status != 200: + raise NetworkError("HTTP callback got status {status} for URL {url}" + .format(status=httpResponse.status, url=url)) + + try: + with open(self.dst, 'wb') as f: + f.write(httpResponse.read()) + except HTTPException as exc: + raise NetworkError("for URL {url}: {error}" + .format(url=url, error=exc)) from exc + + if self.mycallback != None: + self.mycallback(self) + + +class HTTPSocketRequest(HTTPGetCallback): + """HTTPGetCallback class whose callback returns a file-like object. + + The file-like object returned by the callback, and thus by + HTTPGetter.get(), is a socket or similar. This allows one to read + the data obtained from the network without necessarily storing it + to a file. + + """ + def __init__(self, src): + """Initialize an HTTPSocketRequest object. + + src -- VirtualPath instance for the resource on the server + (presumably a file) + + """ + HTTPGetCallback.__init__(self, src, self.callback) + + def callback(self, url, httpResponse): + # Same comment as for HTTPDownloadRequest.callback() + if httpResponse.status != 200: + raise NetworkError("HTTP callback got status {status} for URL {url}" + .format(status=httpResponse.status, url=url)) + + return httpResponse + +################################################################################################################################# + +class Coordinate: + def __init__(self, lat, lon): + self.lat = lat + self.lon = lon + +class DownloadBoundaries: + def __init__(self, top, left, bottom, right): + if top < bottom: + raise ValueError("top cannot be less than bottom") + if right < left: + raise ValueError("right cannot be less than left") + + if top > 90 or bottom < -90: + raise ValueError("top and bottom must be a valid latitude") + if left < -180 or right > 180: + raise ValueError("left and right must be a valid longitude") + self.top = top + self.left = left + self.bottom = bottom + self.right = right + + def is_coordinate_inside_boundaries(self, coordinate): + if coordinate.lat < self.bottom or coordinate.lat > self.top: + return False + if coordinate.lon < self.left or coordinate.lon > self.right: + return False + return True + +def parse_terrasync_coordinate(coordinate): + matches = re.match("(w|e)(\d{3})(n|s)(\d{2})", coordinate) + if not matches: + return None + lon = int(matches.group(2)) + if matches.group(1) == "w": + lon *= -1 + lat = int(matches.group(4)) + if matches.group(3) == "s": + lat *= -1 + return Coordinate(lat, lon) + + +class Report: + """Gather and format data about the state of a TerraSync mirror.""" + + def __init__(self, targetDir): + self.targetDir = targetDir + + self.dirsWithMissingIndex = set() + self.dirsWithMismatchingDirIndexHash = set() + self.missingFiles = set() + self.filesWithMismatchingHash = set() + self.dirsSkippedDueToBoundaries = set() + + self.orphanFiles = set() + self.orphanDirs = set() + + def addMissingDirIndex(self, directoryVirtualPath): + self.dirsWithMissingIndex.add(directoryVirtualPath) + + def addDirIndexWithMismatchingHash(self, directoryVirtualPath): + self.dirsWithMismatchingDirIndexHash.add(directoryVirtualPath) + + def addMissingFile(self, virtualPath): + self.missingFiles.add(virtualPath) + + def addFileWithMismatchingHash(self, virtualPath): + self.filesWithMismatchingHash.add(virtualPath) + + def addSkippedDueToBoundaries(self, virtualPath): + self.dirsSkippedDueToBoundaries.add(virtualPath) + + def addOrphanFile(self, virtualPath): + self.orphanFiles.add(virtualPath) + + def addOrphanDir(self, virtualPath): + self.orphanDirs.add(virtualPath) + + def summaryString(self): + reportElements = [ + ("Directories with missing index", self.dirsWithMissingIndex), + ("Directories whose .dirindex file had a mismatching hash", + self.dirsWithMismatchingDirIndexHash), + ("Missing files", self.missingFiles), + ("Files with a mismatching hash", self.filesWithMismatchingHash), + ("Directories skipped because of the specified boundaries", + self.dirsSkippedDueToBoundaries), + ("Orphan files", self.orphanFiles), + ("Orphan directories", self.orphanDirs)] + + l = [] + for heading, setOfFilesOrDirs in reportElements: + if setOfFilesOrDirs: + l.append(heading + ":\n") + l.extend( (" " + str(f) for f in sorted(setOfFilesOrDirs)) ) + l.append('') # ensure a blank line follows the list + else: + l.append(heading + ": none") + + return '\n'.join(l) + + def printReport(self): + title = "{prg} report".format(prg=PROGNAME) + print("\n" + title + '\n' + len(title)*"=", end="\n\n") + print(self.summaryString()) + + +@enum.unique +class FailedCheckReason(enum.Enum): + """Reasons that can cause 'check' mode to report a mismatch. + + Note that network errors and things like that do *not* belong here. + + """ + + missingDirIndexFile, mismatchingHashForDirIndexFile, \ + missingNormalFile, mismatchingHashForNormalFile, \ + orphanFile, orphanDirectory = range(6) + + # 'path': VirtualPath instance for a file or directory + def explain(self, path): + if self is FailedCheckReason.missingDirIndexFile: + res = ".dirindex file '{}' is missing locally".format(path) + elif self is FailedCheckReason.mismatchingHashForDirIndexFile: + res = ".dirindex file '{}' doesn't have the hash it " \ + "should have according to the server".format(path) + elif self is FailedCheckReason.missingNormalFile: + res = "file '{}' is present on the server but missing locally" \ + .format(path) + elif self is FailedCheckReason.mismatchingHashForNormalFile: + res = "file '{}' doesn't have the hash given in the " \ + ".dirindex file of its containing directory".format(path) + elif self is FailedCheckReason.orphanFile: + res = "file '{}' was found locally but is not present on the " \ + "server".format(path) + elif self is FailedCheckReason.orphanDirectory: + res = "directory '{}' was found locally but is not present " \ + "on the server".format(path) + else: + assert False, "Unhandled enum value: {!r}".format(self) + + return res + + +class TerraSync: + + @enum.unique + class Mode(enum.Enum): + """Main modes of operation for the TerraSync class.""" + + # Using lower case for the member names, because this way + # enumMember.name is exactly the mode string passed to --mode on the + # command line (can be useful for messages destined to users). + check, sync = range(2) + + def __init__(self, mode, doReport, url, target, quick, removeOrphan, + downloadBoundaries): + self.mode = self.Mode[mode] + self.doReport = doReport + self.setUrl(url).setTarget(target) + self.quick = quick + self.removeOrphan = removeOrphan + self.httpGetter = None + self.downloadBoundaries = downloadBoundaries + # Status of the local repository (as compared to what the server says), + # before any update we might do to it. + self.report = Report(self.target) + + def inSyncMode(self): + return self.mode == self.Mode.sync + + def setUrl(self, url): + self.url = url.rstrip('/').strip() + return self + + def setTarget(self, target): + # Using os.path.abspath() here is safer in case the process later uses + # os.chdir(), which would change the meaning of the "." directory. + self.target = os.path.abspath(target) + return self + + def start(self, virtualSubdir=VirtualPath('/')): + """Start the 'sync' or 'check' process. + + The 'virtualSubdir' argument must be a VirtualPath instance and + allows one to start the 'sync' or 'check' process in a chosen + subdirectory of the TerraSync repository, instead of at its + root. + + """ + # Remove the leading '/' from 'virtualSubdir' and convert to native + # separators ('/' or '\' depending on the platform). + localSubdir = os.path.normpath(virtualSubdir.asRelative()) + if localSubdir == ".": # just ugly, but it wouldn't hurt + localSubdir = "" + + assert not os.path.isabs(localSubdir), repr(localSubdir) + self.httpGetter = HTTPGetter(self.url) + + # Get the hash of the .dirindex file for 'virtualSubdir' + try: + request = HTTPSocketRequest(virtualSubdir / ".dirindex") + with self.httpGetter.get(request) as fileLike: + dirIndexHash = computeHash(fileLike) + except HTTPException as exc: + raise NetworkError("for the root .dirindex file: {errMsg}" + .format(errMsg=exc)) from exc + + # Process the chosen part of the repository (recursive) + self.processDirectoryEntry(virtualSubdir, localSubdir, dirIndexHash) + + return self.report + + def processFileEntry(self, virtualPath, localPath, fileHash): + """Process a file entry from a .dirindex file.""" + localFullPath = join(self.target, localPath) + failedCheckReason = None + + if not os.path.isfile(localFullPath): + self.report.addMissingFile(virtualPath) + failedCheckReason = FailedCheckReason.missingNormalFile + elif hashForFile(localFullPath) != fileHash: + self.report.addFileWithMismatchingHash(virtualPath) + failedCheckReason = FailedCheckReason.mismatchingHashForNormalFile + else: + # The file exists and has the hash mentioned in the .dirindex file + return + + assert failedCheckReason is not None + + if self.inSyncMode(): + if os.path.isdir(localFullPath): + # 'localFullPath' is a directory (locally), but on the server + # it is a file -> remove the dir so that we can store the file. + removeDirectoryTree(self.target, localFullPath) + + print("Downloading '{}'".format(virtualPath)) + request = HTTPDownloadRequest(self, virtualPath, localFullPath) + self.httpGetter.get(request) + else: + self.abortCheckMode(failedCheckReason, virtualPath) + + def processDirectoryEntry(self, virtualPath, localPath, dirIndexHash): + """Process a directory entry from a .dirindex file.""" + print("Processing '{}'...".format(virtualPath)) + + coord = parse_terrasync_coordinate(virtualPath.name) + if (coord and + not self.downloadBoundaries.is_coordinate_inside_boundaries(coord)): + self.report.addSkippedDueToBoundaries(virtualPath) + return + + localFullPath = join(self.target, localPath) + localDirIndex = join(localFullPath, ".dirindex") + failedCheckReason = None + + if not os.path.isfile(localDirIndex): + failedCheckReason = FailedCheckReason.missingDirIndexFile + self.report.addMissingDirIndex(virtualPath) + elif hashForFile(localDirIndex) != dirIndexHash: + failedCheckReason = FailedCheckReason.mismatchingHashForDirIndexFile + self.report.addDirIndexWithMismatchingHash(virtualPath) + + if failedCheckReason is None: + if not self.quick: + self.handleDirindexFile(localDirIndex) + elif self.inSyncMode(): + if not os.path.exists(localFullPath): + os.makedirs(localFullPath) + + request = HTTPDownloadRequest(self, + virtualPath / ".dirindex", + localDirIndex, + self.handleDirindexRequest) + self.httpGetter.get(request) + else: + self.abortCheckMode(failedCheckReason, virtualPath / ".dirindex") + + def handleDirindexRequest(self, dirindexRequest): + self.handleDirindexFile(dirindexRequest.dst) + + def handleDirindexFile(self, dirindexFile): + dirIndex = DirIndex(dirindexFile) + virtualBase = dirIndex.getPath() # VirtualPath instance + relativeBase = virtualBase.asRelative() # string, doesn't start with '/' + serverFiles = [] + serverDirs = [] + + for file in dirIndex.getFiles(): + f = file['name'] + self.processFileEntry(virtualBase / f, + join(relativeBase, f), + file['hash']) + serverFiles.append(f) + + for subdir in dirIndex.getDirectories(): + d = subdir['name'] + self.processDirectoryEntry(virtualBase / d, + join(relativeBase, d), + subdir['hash']) + serverDirs.append(d) + + localFullPath = join(self.target, relativeBase) + localFiles = [ f for f in listdir(localFullPath) + if isfile(join(localFullPath, f)) ] + + for f in localFiles: + if f != ".dirindex" and f not in serverFiles: + virtualPath = virtualBase / f + self.report.addOrphanFile(virtualPath) + + if self.inSyncMode(): + if self.removeOrphan: + os.remove(join(self.target, virtualPath.asRelative())) + else: + self.abortCheckMode(FailedCheckReason.orphanFile, + virtualPath) + + localDirs = [ f for f in listdir(localFullPath) + if isdir(join(localFullPath, f)) ] + + for d in localDirs: + if d not in serverDirs: + virtualPath = virtualBase / d + self.report.addOrphanDir(virtualPath) + + if self.inSyncMode(): + if self.removeOrphan: + removeDirectoryTree(self.target, + join(self.target, + virtualPath.asRelative())) + else: + self.abortCheckMode(FailedCheckReason.orphanDirectory, + virtualPath) + + # 'reason' is a member of the FailedCheckReason enum + def abortCheckMode(self, reason, fileOrDirVirtualPath): + assert self.mode == self.Mode.check, repr(self.mode) + + print("{prg}: exiting from 'check' mode because {explanation}." + .format(prg=PROGNAME, + explanation=reason.explain(fileOrDirVirtualPath))) + + if self.doReport: + self.report.printReport() + + sys.exit(ExitStatus.CHECK_MODE_FOUND_MISMATCH.value) + +################################################################################################################################# + +def parseCommandLine(): + parser = argparse.ArgumentParser() + + parser.add_argument("-u", "--url", dest="url", metavar="URL", + default="http://flightgear.sourceforge.net/scenery", + help="server URL [default: %(default)s]") + + parser.add_argument("-t", "--target", dest="target", metavar="DIR", + default=".", help="""\ + directory where to store the files [default: the current directory]""") + + parser.add_argument("--only-subdir", dest="onlySubdir", metavar="SUBDIR", + default="", help="""\ + restrict processing to this subdirectory of the TerraSync repository. Use + a path relative to the repository root, for instance 'Models/Residential' + [default: process the whole repository]""") + + parser.add_argument("-q", "--quick", dest="quick", action="store_true", + default=False, help="enable quick mode") + + parser.add_argument("-r", "--remove-orphan", dest="removeOrphan", + action="store_true", + default=False, help="remove old scenery files") + + parser.add_argument("--mode", default="sync", choices=("check", "sync"), + help="""\ + main mode of operation (default: '%(default)s'). In 'sync' mode, contents + is downloaded from the server to the target directory. On the other hand, + in 'check' mode, {progname} compares the contents of the target directory + with the remote repository without writing nor deleting anything on + disk.""".format(progname=PROGNAME)) + + parser.add_argument("--report", dest="report", action="store_true", + default=False, + help="""\ + before normal exit, print a report of what was found""") + + parser.add_argument("--top", dest="top", type=int, default=90, help="""\ + maximum latitude to include in download [default: %(default)d]""") + + parser.add_argument("--bottom", dest="bottom", type=int, default=-90, + help="""\ + minimum latitude to include in download [default: %(default)d]""") + + parser.add_argument("--left", dest="left", type=int, default=-180, help="""\ + minimum longitude to include in download [default: %(default)d]""") + parser.add_argument("--right", dest="right", type=int, default=180, + help="""\ + maximum longitude to include in download [default: %(default)d]""") + + args = parser.parse_args() + + # Perform consistency checks on the arguments + if args.mode == "check" and args.removeOrphan: + print("{prg}: 'check' mode is read-only and thus doesn't make sense " + "with\noption --remove-orphan (-r)".format(prg=PROGNAME), + file=sys.stderr) + sys.exit(ExitStatus.ERROR.value) + + # Replace backslashes with forward slashes, remove leading and trailing + # slashes, collapse consecutive slashes. Yes, this implies that we tolerate + # leading slashes for --only-subdir (which makes sense because virtual + # paths are printed like that by this program, therefore it is natural for + # users to copy & paste such paths in order to use them for --only-subdir). + args.virtualSubdir = VirtualPath(args.onlySubdir.replace('\\', '/')) + + # Be nice to our user in case the path starts with '\', 'C:\', etc. + if os.path.isabs(args.virtualSubdir.asRelative()): + print("{prg}: option --only-subdir expects a *relative* path, but got " + "'{subdir}'".format(prg=PROGNAME, subdir=args.onlySubdir), + file=sys.stderr) + sys.exit(ExitStatus.ERROR.value) + + return args + + +def main(): + args = parseCommandLine() + terraSync = TerraSync(args.mode, args.report, args.url, args.target, + args.quick, args.removeOrphan, + DownloadBoundaries(args.top, args.left, args.bottom, + args.right)) + report = terraSync.start(args.virtualSubdir) + + if args.report: + report.printReport() + + sys.exit(ExitStatus.SUCCESS.value) diff -Nru flightgear-2017.3.1+dfsg/scripts/python/TerraSync/terrasync/virtual_path.py flightgear-2018.1.1+dfsg/scripts/python/TerraSync/terrasync/virtual_path.py --- flightgear-2017.3.1+dfsg/scripts/python/TerraSync/terrasync/virtual_path.py 1970-01-01 00:00:00.000000000 +0000 +++ flightgear-2018.1.1+dfsg/scripts/python/TerraSync/terrasync/virtual_path.py 2018-04-06 19:43:33.000000000 +0000 @@ -0,0 +1,503 @@ +# -*- coding: utf-8 -*- + +# virtual_path.py --- Classes used to manipulate slash-separated virtual paths +# +# Copyright (C) 2018 Florent Rougon +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; either version 2 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +"""Module containing the VirtualPath and MutableVirtualPath classes.""" + +import pathlib + + +class VirtualPath: + """Class used to represent virtual paths using the slash separator. + + This class always uses the slash ('/') as the separator between + components. For terrasync.py, the root path '/' corresponds to the + repository root, regardless of where it is stored (hard drive, + remote server, etc.). + + Note: because of this, the class is not supposed to be used directly + for filesystem accesses, since some root directory or + protocol://server/root-dir prefix would have to be prepended + to provide reasonably useful functionality. This is why the + paths managed by this class are said to be virtual. This also + implies that even in Python 3.6 or later, this class should + *not* inherit from os.PathLike. + + Whenever a given feature exists in pathlib.PurePath, this class + replicates the corresponding pathlib.PurePath API, but using + mixedCaseStyle instead of underscore_style (the latter being used + for every method of pathlib.PurePath). Of course, types are adapted: + for instance, methods of this class often return a VirtualPath + instance, whereas the corresponding pathlib.PurePath methods would + return a pathlib.PurePath instance. + + """ + def __init__(self, p): + # Once this function exits, self._path *must not be changed* anymore + # (doing so would violate the contract for a hashable object: the + # hash must not change once the object has been constructed). + self._path = self.normalizeStringPath(p) + # This check could of course be skipped if it is found to really affect + # performance. + self._check() + + def __str__(self): + """Return a string representation of the path in self. + + The return value: + - always starts with a '/'; + - never ends with a '/' except if it is exactly '/' (i.e., + the root virtual path). + + """ + return self._path + + def asPosix(self): + """Return a string representation of the path in self. + + This method returns str(self), it is only present for + compatibility with pathlib.PurePath. + + """ + return str(self) + + def __repr__(self): + return "{}.{}({!r})".format(__name__, type(self).__name__, self._path) + + def __lt__(self, other): + # Allow sorting with instances of VirtualPath, or of any subclass. Note + # that the == operator (__eq__()) and therefore also != are stricter + # with respect to typing. + if isinstance(other, VirtualPath): + return self._path < other._path + else: + return NotImplemented + + def __le__(self, other): + if isinstance(other, VirtualPath): + return self._path <= other._path + else: + return NotImplemented + + def __eq__(self, other): + # The types must be the same, therefore a VirtualPath never compares + # equal to a MutableVirtualPath with the == operator. For such + # comparisons, use the samePath() method. If __eq__() (and thus + # necessarily __hash__()) were more lax about typing, adding + # VirtualPath instances and instances of hashable subclasses of + # VirtualPath with the same _path to a set or frozenset would lead to + # unintuitive behavior, since they would all be considered equal. + return type(self) == type(other) and self._path == other._path + + def __gt__(self, other): + if isinstance(other, VirtualPath): + return self._path > other._path + else: + return NotImplemented + + def __ge__(self, other): + if isinstance(other, VirtualPath): + return self._path >= other._path + else: + return NotImplemented + + def __hash__(self): + # Be strict about typing, as for __eq__(). + return hash((type(self), self._path)) + + def samePath(self, other): + """Compare the path with another instance, possibly of a subclass. + + other -- instance of VirtualPath, or of a subclass of + VirtualPath + + """ + if isinstance(other, VirtualPath): + return self._path == other._path + else: + raise TypeError("{obj!r} is of type {klass}, which is neither " + "VirtualPath nor a subclass thereof" + .format(obj=other, klass=type(other).__name__)) + + def _check(self): + """Run consistency checks on self.""" + assert (self._path.startswith('/') and not self._path.startswith('//') + and (self._path == '/' or not self._path.endswith('/'))), \ + repr(self._path) + + @classmethod + def normalizeStringPath(cls, path): + """Normalize a string representing a virtual path. + + path -- input path (string) + + Return a string that always starts with a slash, never contains + consecutive slashes and only ends with a slash if it's the root + virtual path ('/'). + + If 'path' doesn't start with a slash ('/'), it is considered + relative to the root. This implies that if 'path' is the empty + string, the return value is '/'. + + """ + if not path.startswith('/'): + # / is the “virtual root” of the TerraSync repository + path = '/' + path + elif path.startswith('//') and not path.startswith('///'): + # Nasty special case. As allowed (but not mandated!) by POSIX[1], + # in pathlib.PurePosixPath('//some/path'), no collapsing happens[2]. + # This is only the case for exactly *two* *leading* slashes. + # [1] http://pubs.opengroup.org/onlinepubs/009695399/basedefs/xbd_chap04.html#tag_04_11 + # [2] https://www.python.org/dev/peps/pep-0428/#construction + path = path[1:] + + return pathlib.PurePosixPath(path).as_posix() + + def __truediv__(self, s): + """Path concatenation with the '/' operator. + + 's' must be a string representing a relative path using the '/' + separator, for instance "dir/subdir/other-subdir". + + Return a new instance of type(self). + + """ + assert not (s.startswith('/') or s.endswith('/')), repr(s) + + if self._path == '/': + return type(self)(self._path + s) + else: + return type(self)(self._path + '/' + s) + + def joinpath(self, *args): + """Combine 'self' with each given string argument in turn. + + Each argument should be of the form "foo", "foo/bar", + "foo/bar/baz", etc. Return the corresponding instance of + type(self). + + >>> p = VirtualPath("/foo").joinpath("bar", "baz", "quux/zoot") + >>> str(p) + '/foo/bar/baz/quux/zoot' + + """ + return self / '/'.join(args) + + @property + def name(self): + """Return a string representing the final path component. + + >>> p = VirtualPath("/foo/bar/baz") + >>> p.name + 'baz' + + """ + pos = self._path.rfind('/') + assert pos != -1, (pos, self._path) + + return self._path[pos+1:] + + @property + def parts(self): + """Return a tuple containing the path’s components. + + >>> p = VirtualPath('/usr/bin/python3') + >>> p.parts + ('/', 'usr', 'bin', 'python3') + + """ + if self._path == "/": + return ('/',) + else: + # Skip the leading slash before splitting + return ('/',) + tuple(self._path[1:].split('/')) + + def generateParents(self): + """Generator function for the parents of the path. + + See the 'parents' property for details. + + """ + if self._path == '/': + return + + assert self._path.startswith('/'), repr(self._path) + prevPos = len(self._path) + + while True: + pos = self._path.rfind('/', 0, prevPos) + + if pos > 0: + yield type(self)(self._path[:pos]) + prevPos = pos + else: + assert pos == 0, pos + break + + yield type(self)('/') + + @property + def parents(self): + """The path ancestors. + + Return an immutable sequence providing access to the logical + ancestors of the path. + + >>> p = VirtualPath('/foo/bar/baz') + >>> len(p.parents) + 3 + >>> p.parents[0] + terrasync.virtual_path.VirtualPath('/foo/bar') + >>> p.parents[1] + terrasync.virtual_path.VirtualPath('/foo') + >>> p.parents[2] + terrasync.virtual_path.VirtualPath('/') + + """ + return tuple(self.generateParents()) + + @property + def parent(self): + """The logical parent of the path. + + >>> p = VirtualPath('/foo/bar/baz') + >>> p.parent + terrasync.virtual_path.VirtualPath('/foo/bar') + >>> q = VirtualPath('/') + >>> q.parent + terrasync.virtual_path.VirtualPath('/') + + """ + pos = self._path.rfind('/') + assert pos >= 0, pos + + if pos == 0: + return type(self)('/') + else: + return type(self)(self._path[:pos]) + + @property + def suffix(self): + """The extension of the final component, if any. + + >>> VirtualPath('/my/library/setup.py').suffix + '.py' + >>> VirtualPath('/my/library.tar.gz').suffix + '.gz' + >>> VirtualPath('/my/library').suffix + '' + + """ + name = self.name + pos = name.rfind('.') + return name[pos:] if pos != -1 else '' + + @property + def suffixes(self): + """A list of the path’s extensions. + + >>> VirtualPath('/my/library/setup.py').suffixes + ['.py'] + >>> VirtualPath('/my/library.tar.gz').suffixes + ['.tar', '.gz'] + >>> VirtualPath('/my/library').suffixes + [] + + """ + name = self.name + prevPos = len(name) + l = [] + + while True: + pos = name.rfind('.', 0, prevPos) + if pos == -1: + break + else: + l.insert(0, name[pos:prevPos]) + prevPos = pos + + return l + + @property + def stem(self): + """The final path component, without its suffix. + + >>> VirtualPath('/my/library.tar.gz').stem + 'library.tar' + >>> VirtualPath('/my/library.tar').stem + 'library' + >>> VirtualPath('/my/library').stem + 'library' + >>> VirtualPath('/').stem + '' + + """ + name = self.name + pos = name.rfind('.') + + return name if pos == -1 else name[:pos] + + def asRelative(self): + """Return the virtual path without its leading '/'. + + >>> p = VirtualPath('/usr/bin/python3') + >>> p.asRelative() + 'usr/bin/python3' + + >>> VirtualPath('').asRelative() + '' + >>> VirtualPath('/').asRelative() + '' + + """ + assert self._path.startswith('/'), repr(self._path) + return self._path[1:] + + def relativeTo(self, other): + """Return the portion of this path that follows 'other'. + + The return value is a string. If the operation is impossible, + ValueError is raised. + + >>> VirtualPath('/etc/passwd').relativeTo('/') + 'etc/passwd' + >>> VirtualPath('/etc/passwd').relativeTo('/etc') + 'passwd' + + """ + normedOther = self.normalizeStringPath(other) + + if normedOther == '/': + return self._path[1:] + elif self._path.startswith(normedOther): + rest = self._path[len(normedOther):] + + if rest.startswith('/'): + return rest[1:] + + raise ValueError("{!r} does not start with '{}'".format(self, other)) + + def withName(self, newName): + """Return a new VirtualPath instance with the 'name' part changed. + + If the original path is '/' (which doesn’t have a name in the + sense of the 'name' property), ValueError is raised. + + >>> p = VirtualPath('/foobar/downloads/pathlib.tar.gz') + >>> p.withName('setup.py') + terrasync.virtual_path.VirtualPath('/foobar/downloads/setup.py') + + """ + if self._path == '/': + raise ValueError("{!r} has an empty name".format(self)) + else: + pos = self._path.rfind('/') + assert pos != -1, (pos, self._path) + + if newName.startswith('/'): + raise ValueError("{!r} starts with a '/'".format(newName)) + elif newName.endswith('/'): + raise ValueError("{!r} ends with a '/'".format(newName)) + else: + return VirtualPath(self._path[:pos]) / newName + + + def withSuffix(self, newSuffix): + """Return a new VirtualPath instance with the suffix changed. + + If the original path doesn’t have a suffix, the new suffix is + appended: + + >>> p = VirtualPath('/foobar/downloads/pathlib.tar.gz') + >>> p.withSuffix('.bz2') + terrasync.virtual_path.VirtualPath('/foobar/downloads/pathlib.tar.bz2') + >>> p = VirtualPath('/foobar/README') + >>> p.withSuffix('.txt') + terrasync.virtual_path.VirtualPath('/foobar/README.txt') + + If 'self' is the root virtual path ('/') or 'newSuffix' doesn't + start with '.', ValueError is raised. + + """ + if not newSuffix.startswith('.'): + raise ValueError("new suffix {!r} doesn't start with '.'" + .format(newSuffix)) + + name = self.name + if not name: + raise ValueError("{!r} has an empty 'name' part".format(self)) + + pos = name.rfind('.') + + if pos == -1: + return self.withName(name + newSuffix) # append suffix + else: + return self.withName(name[:pos] + newSuffix) # replace suffix + + +class MutableVirtualPath(VirtualPath): + + """Mutable subclass of VirtualPath. + + Contrary to VirtualPath objects, instances of this class can be + modified in-place with the /= operator, in order to append path + components. The price to pay for this advantage is that they can't + be used as dictionary keys or as elements of a set or frozenset, + because they are not hashable. + + """ + + __hash__ = None # ensure the type is not hashable + + def _normalize(self): + self._path = self.normalizeStringPath(self._path) + + def __itruediv__(self, s): + """Path concatenation with the '/=' operator. + + 's' must be a string representing a relative path using the '/' + separator, for instance "dir/subdir/other-subdir". + + """ + # This check could of course be skipped if it is found to really affect + # performance. + self._check() + assert not (s.startswith('/') or s.endswith('/')), repr(s) + + if self._path == '/': + self._path += s + else: + self._path += '/' + s + + # Collapse multiple slashes, remove trailing '/' except if the whole + # path is '/', etc. + self._normalize() + + return self + + +if __name__ == "__main__": + # The doctest setup below works, but for full test coverage, use the + # unittest framework (it is set up to automatically run all doctests from + # this module!). + # + # Hint: 'python3 -m unittest discover' from the TerraSync directory + # should do the trick. + import doctest + doctest.testmod() diff -Nru flightgear-2017.3.1+dfsg/scripts/python/TerraSync/terrasync.py flightgear-2018.1.1+dfsg/scripts/python/TerraSync/terrasync.py --- flightgear-2017.3.1+dfsg/scripts/python/TerraSync/terrasync.py 1970-01-01 00:00:00.000000000 +0000 +++ flightgear-2018.1.1+dfsg/scripts/python/TerraSync/terrasync.py 2018-04-06 19:43:33.000000000 +0000 @@ -0,0 +1,26 @@ +#! /usr/bin/env python3 +# -*- coding: utf-8 -*- + +# terrasync.py --- Synchronize TerraScenery data to your local disk +# Copyright (C) 2018 Florent Rougon +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; either version 2 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +# terrasync.py development was started by Torsten Dreyer in 2016. This file is +# just the normal entry point for users. + +import terrasync.main + +terrasync.main.main() diff -Nru flightgear-2017.3.1+dfsg/scripts/python/TerraSync/tests/test_virtual_path.py flightgear-2018.1.1+dfsg/scripts/python/TerraSync/tests/test_virtual_path.py --- flightgear-2017.3.1+dfsg/scripts/python/TerraSync/tests/test_virtual_path.py 1970-01-01 00:00:00.000000000 +0000 +++ flightgear-2018.1.1+dfsg/scripts/python/TerraSync/tests/test_virtual_path.py 2018-04-06 19:43:33.000000000 +0000 @@ -0,0 +1,361 @@ +#! /usr/bin/env python3 +# -*- coding: utf-8 -*- + +# test_virtual_path.py --- Test module for terrasync.virtual_path +# Copyright (C) 2018 Florent Rougon +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; either version 2 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +# In order to exercise all tests, run the following command from the parent +# directory (you may omit the 'discover' argument): +# +# python3 -m unittest discover + +import collections +import unittest + +from terrasync.virtual_path import VirtualPath, MutableVirtualPath + +# Hook doctest-based tests into the unittest test discovery mechanism +import doctest +import terrasync.virtual_path + +def load_tests(loader, tests, ignore): + # Tell unittest to run doctests from terrasync.virtual_path + tests.addTests(doctest.DocTestSuite(terrasync.virtual_path)) + return tests + + +class VirtualPathCommonTests: + """Common tests to run for both VirtualPath and MutableVirtualPath. + + The tests inside this class must exercice the class (VirtualPath or + MutableVirtualPath) stored in the 'cls' class attribute. They must + work for both VirtualPath and MutableVirtualPath, otherwise they + don't belong here! + + """ + + def test_normalizeStringPath(self): + self.assertEqual(self.cls.normalizeStringPath("/"), "/") + self.assertEqual(self.cls.normalizeStringPath(""), "/") + self.assertEqual( + self.cls.normalizeStringPath("/abc/Def ijk//l Mn///op/q/rst/"), + "/abc/Def ijk/l Mn/op/q/rst") + self.assertEqual(self.cls.normalizeStringPath("abc/def"), "/abc/def") + self.assertEqual(self.cls.normalizeStringPath("/abc/def"), "/abc/def") + self.assertEqual(self.cls.normalizeStringPath("//abc/def"), + "/abc/def") + self.assertEqual(self.cls.normalizeStringPath("///abc/def"), + "/abc/def") + self.assertEqual(self.cls.normalizeStringPath("/abc//def"), + "/abc/def") + + # Unless the implementation of VirtualPath.__init__() has changed + # meanwhile, the following function must be essentially the same as + # test_normalizeStringPath(). + def test_constructor_and_str(self): + p = self.cls("/") + self.assertEqual(str(p), "/") + + p = self.cls("") + self.assertEqual(str(p), "/") + + p = self.cls("/abc/Def ijk//l Mn///op/q/rst/") + self.assertEqual(str(p), "/abc/Def ijk/l Mn/op/q/rst") + + p = self.cls("abc/def") + self.assertEqual(str(p), "/abc/def") + + p = self.cls("/abc/def") + self.assertEqual(str(p), "/abc/def") + + p = self.cls("//abc/def") + self.assertEqual(str(p), "/abc/def") + + p = self.cls("///abc/def") + self.assertEqual(str(p), "/abc/def") + + p = self.cls("/abc//def") + self.assertEqual(str(p), "/abc/def") + + def test_asPosix (self): + self.assertEqual(self.cls("").asPosix(), "/") + self.assertEqual(self.cls("/").asPosix(), "/") + self.assertEqual(self.cls("/abc//def").asPosix(), "/abc/def") + self.assertEqual(self.cls("/abc//def/").asPosix(), "/abc/def") + self.assertEqual(self.cls("//abc//def//").asPosix(), "/abc/def") + self.assertEqual(self.cls("////abc//def//").asPosix(), "/abc/def") + + def test_samePath(self): + self.assertTrue(self.cls("").samePath(self.cls(""))) + self.assertTrue(self.cls("").samePath(self.cls("/"))) + self.assertTrue(self.cls("/").samePath(self.cls(""))) + self.assertTrue(self.cls("/").samePath(self.cls("/"))) + + self.assertTrue( + self.cls("/abc/def").samePath(self.cls("/abc/def"))) + self.assertTrue( + self.cls("/abc//def").samePath(self.cls("/abc/def"))) + self.assertTrue( + self.cls("/abc/def/").samePath(self.cls("/abc/def"))) + + def test_comparisons(self): + self.assertEqual(self.cls("/abc/def"), self.cls("/abc/def")) + self.assertEqual(self.cls("/abc//def"), self.cls("/abc/def")) + self.assertEqual(self.cls("/abc/def/"), self.cls("/abc/def")) + + self.assertNotEqual(self.cls("/abc/dEf"), self.cls("/abc/def")) + self.assertNotEqual(self.cls("/abc/def "), self.cls("/abc/def")) + + self.assertLessEqual(self.cls("/foo/bar"), self.cls("/foo/bar")) + self.assertLessEqual(self.cls("/foo/bar"), self.cls("/foo/bbr")) + self.assertLess(self.cls("/foo/bar"), self.cls("/foo/bbr")) + + self.assertGreaterEqual(self.cls("/foo/bar"), self.cls("/foo/bar")) + self.assertGreaterEqual(self.cls("/foo/bbr"), self.cls("/foo/bar")) + self.assertGreater(self.cls("/foo/bbr"), self.cls("/foo/bar")) + + def test_truedivOperators(self): + """ + Test operators used to add paths components to a VirtualPath instance.""" + p = self.cls("/foo/bar/baz/quux/zoot") + self.assertEqual(p, self.cls("/") / "foo" / "bar" / "baz/quux/zoot") + self.assertEqual(p, self.cls("/foo") / "bar" / "baz/quux/zoot") + self.assertEqual(p, self.cls("/foo/bar") / "baz/quux/zoot") + + def test_joinpath(self): + p = self.cls("/foo/bar/baz/quux/zoot") + self.assertEqual( + p, + self.cls("/foo").joinpath("bar", "baz", "quux/zoot")) + + def test_nameAttribute(self): + self.assertEqual(self.cls("/").name, "") + + p = self.cls("/foo/bar/baz/quux/zoot") + self.assertEqual(p.name, "zoot") + + def test_partsAttribute(self): + self.assertEqual(self.cls("/").parts, ("/",)) + + p = self.cls("/foo/bar/baz/quux/zoot") + self.assertEqual(p.parts, ("/", "foo", "bar", "baz", "quux", "zoot")) + + def test_parentsAttribute(self): + def pathify(*args): + return tuple( (self.cls(s) for s in args) ) + + p = self.cls("/") + self.assertEqual(tuple(p.parents), pathify()) # empty tuple + + p = self.cls("/foo") + self.assertEqual(tuple(p.parents), pathify("/")) + + p = self.cls("/foo/bar") + self.assertEqual(tuple(p.parents), pathify("/foo", "/")) + + p = self.cls("/foo/bar/baz") + self.assertEqual(tuple(p.parents), pathify("/foo/bar", "/foo", "/")) + + def test_parentAttribute(self): + def pathify(s): + return self.cls(s) + + p = self.cls("/") + self.assertEqual(p.parent, pathify("/")) + + p = self.cls("/foo") + self.assertEqual(p.parent, pathify("/")) + + p = self.cls("/foo/bar") + self.assertEqual(p.parent, pathify("/foo")) + + p = self.cls("/foo/bar/baz") + self.assertEqual(p.parent, pathify("/foo/bar")) + + def test_suffixAttribute(self): + p = self.cls("/") + self.assertEqual(p.suffix, '') + + p = self.cls("/foo/bar/baz.py") + self.assertEqual(p.suffix, '.py') + + p = self.cls("/foo/bar/baz.py.bla") + self.assertEqual(p.suffix, '.bla') + + p = self.cls("/foo/bar/baz") + self.assertEqual(p.suffix, '') + + def test_suffixesAttribute(self): + p = self.cls("/") + self.assertEqual(p.suffixes, []) + + p = self.cls("/foo/bar/baz.py") + self.assertEqual(p.suffixes, ['.py']) + + p = self.cls("/foo/bar/baz.py.bla") + self.assertEqual(p.suffixes, ['.py', '.bla']) + + p = self.cls("/foo/bar/baz") + self.assertEqual(p.suffixes, []) + + def test_stemAttribute(self): + p = self.cls("/") + self.assertEqual(p.stem, '') + + p = self.cls("/foo/bar/baz.py") + self.assertEqual(p.stem, 'baz') + + p = self.cls("/foo/bar/baz.py.bla") + self.assertEqual(p.stem, 'baz.py') + + def test_asRelative(self): + self.assertEqual(self.cls("/").asRelative(), "") + self.assertEqual(self.cls("/foo/bar/baz/quux/zoot").asRelative(), + "foo/bar/baz/quux/zoot") + + def test_relativeTo(self): + self.assertEqual(self.cls("").relativeTo(""), "") + self.assertEqual(self.cls("").relativeTo("/"), "") + self.assertEqual(self.cls("/").relativeTo("/"), "") + self.assertEqual(self.cls("/").relativeTo(""), "") + + p = self.cls("/foo/bar/baz/quux/zoot") + + self.assertEqual(p.relativeTo(""), "foo/bar/baz/quux/zoot") + self.assertEqual(p.relativeTo("/"), "foo/bar/baz/quux/zoot") + + self.assertEqual(p.relativeTo("foo"), "bar/baz/quux/zoot") + self.assertEqual(p.relativeTo("foo/"), "bar/baz/quux/zoot") + self.assertEqual(p.relativeTo("/foo"), "bar/baz/quux/zoot") + self.assertEqual(p.relativeTo("/foo/"), "bar/baz/quux/zoot") + + self.assertEqual(p.relativeTo("foo/bar/baz"), "quux/zoot") + self.assertEqual(p.relativeTo("foo/bar/baz/"), "quux/zoot") + self.assertEqual(p.relativeTo("/foo/bar/baz"), "quux/zoot") + self.assertEqual(p.relativeTo("/foo/bar/baz/"), "quux/zoot") + + with self.assertRaises(ValueError): + p.relativeTo("/foo/ba") + + with self.assertRaises(ValueError): + p.relativeTo("/foo/balloon") + + def test_withName(self): + p = self.cls("/foo/bar/baz/quux/zoot") + + self.assertEqual(p.withName(""), + VirtualPath("/foo/bar/baz/quux")) + self.assertEqual(p.withName("pouet"), + VirtualPath("/foo/bar/baz/quux/pouet")) + self.assertEqual(p.withName("pouet/zdong"), + VirtualPath("/foo/bar/baz/quux/pouet/zdong")) + + # The self.cls object has no 'name' (referring to the 'name' property) + with self.assertRaises(ValueError): + self.cls("").withName("foobar") + + with self.assertRaises(ValueError): + self.cls("/").withName("foobar") + + def test_withSuffix(self): + p = self.cls("/foo/bar/baz.tar.gz") + self.assertEqual(p.withSuffix(".bz2"), + VirtualPath("/foo/bar/baz.tar.bz2")) + p = self.cls("/foo/bar/baz") + self.assertEqual(p.withSuffix(".tar.xz"), + VirtualPath("/foo/bar/baz.tar.xz")) + + # The self.cls object has no 'name' (referring to the 'name' property) + with self.assertRaises(ValueError): + self.cls("/foo/bar/baz.tar.gz").withSuffix("no-leading-dot") + + with self.assertRaises(ValueError): + # The root virtual path ('/') can't be used for this + self.cls("/").withSuffix(".foobar") + + +class TestVirtualPath(unittest.TestCase, VirtualPathCommonTests): + """Tests for the VirtualPath class. + + These are the tests using the common infrastructure from + VirtualPathCommonTests. + + """ + + cls = VirtualPath + +class TestVirtualPathSpecific(unittest.TestCase): + """Tests specific to the VirtualPath class.""" + + def test_isHashableType(self): + p = VirtualPath("/foo") + self.assertTrue(isinstance(p, collections.Hashable)) + + def test_insideSet(self): + l1 = [ VirtualPath("/foo/bar"), + VirtualPath("/foo/baz") ] + l2 = l1 + [ VirtualPath("/foo/bar") ] # l2 has a duplicate element + + # Sets allow one to ignore duplicate elements when comparing + self.assertEqual(set(l1), set(l2)) + self.assertEqual(frozenset(l1), frozenset(l2)) + + +class TestMutableVirtualPath(unittest.TestCase, VirtualPathCommonTests): + """Tests for the MutableVirtualPath class. + + These are the tests using the common infrastructure from + VirtualPathCommonTests. + + """ + + cls = MutableVirtualPath + +class TestMutableVirtualPathSpecific(unittest.TestCase): + """Tests specific to the MutableVirtualPath class.""" + + def test_mixedComparisons(self): + self.assertTrue( + VirtualPath("/abc/def").samePath(MutableVirtualPath("/abc/def"))) + self.assertTrue( + VirtualPath("/abc//def").samePath(MutableVirtualPath("/abc/def"))) + self.assertTrue( + VirtualPath("/abc/def/").samePath(MutableVirtualPath("/abc/def"))) + + self.assertTrue( + MutableVirtualPath("/abc/def").samePath(VirtualPath("/abc/def"))) + self.assertTrue( + MutableVirtualPath("/abc//def").samePath(VirtualPath("/abc/def"))) + self.assertTrue( + MutableVirtualPath("/abc/def/").samePath(VirtualPath("/abc/def"))) + + def test_inPlacePathConcatenation(self): + p = VirtualPath("/foo/bar/baz/quux/zoot") + + q = MutableVirtualPath("/foo") + q /= "bar" + q /= "baz/quux/zoot" + + self.assertTrue(p.samePath(q)) + + def test_isNotHashableType(self): + p = MutableVirtualPath("/foo") + self.assertFalse(isinstance(p, collections.Hashable)) + + +if __name__ == "__main__": + unittest.main() diff -Nru flightgear-2017.3.1+dfsg/scripts/python/terrasync.py flightgear-2018.1.1+dfsg/scripts/python/terrasync.py --- flightgear-2017.3.1+dfsg/scripts/python/terrasync.py 2017-09-20 07:51:30.000000000 +0000 +++ flightgear-2018.1.1+dfsg/scripts/python/terrasync.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,314 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# -# Copyright (C) 2016 Torsten Dreyer -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License as -# published by the Free Software Foundation; either version 2 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# terrasync.py - synchronize terrascenery data to your local disk -# needs dnspython (pip install dnspython) - -import urllib, os, hashlib -from urllib.parse import urlparse -from http.client import HTTPConnection, _CS_IDLE, HTTPException -from os import listdir -from os.path import isfile, isdir, join -import re -import argparse -import shutil - -################################################################################################################################# -class HTTPGetCallback: - def __init__(self, src, callback): - self.callback = callback - self.src = src - self.result = None - -class HTTPGetter: - def __init__(self, baseUrl, maxPending=10): - self.baseUrl = baseUrl - self.parsedBaseUrl = urlparse(baseUrl) - self.maxPending = maxPending - self.requests = [] - self.pendingRequests = [] - self.httpConnection = HTTPConnection(self.parsedBaseUrl.netloc) - self.httpRequestHeaders = headers = {'Host':self.parsedBaseUrl.netloc,'Content-Length':0,'Connection':'Keep-Alive','User-Agent':'FlightGear terrasync.py'} - - def doGet(self, httpGetCallback): - conn = self.httpConnection - request = httpGetCallback - self.httpConnection.request("GET", self.parsedBaseUrl.path + request.src, None, self.httpRequestHeaders) - httpGetCallback.result = self.httpConnection.getresponse() - httpGetCallback.callback() - - def get(self, httpGetCallback): - - try: - self.doGet(httpGetCallback) - except HTTPException: - # try to reconnect once - #print("reconnect") - self.httpConnection.close() - self.httpConnection.connect() - self.doGet(httpGetCallback) - - -################################################################################################################################# -class DirIndex: - - def __init__(self, dirIndexFile): - self.d = [] - self.f = [] - self.version = 0 - self.path = "" - - with open(dirIndexFile) as f: - self.readFrom(f) - - def readFrom(self, readable): - for line in readable: - line = line.strip() - if line.startswith('#'): - continue - - tokens = line.split(':') - if len(tokens) == 0: - continue - - if tokens[0] == "version": - self.version = int(tokens[1]) - - elif tokens[0] == "path": - self.path = tokens[1] - - elif tokens[0] == "d": - self.d.append({ 'name': tokens[1], 'hash': tokens[2] }) - - elif tokens[0] == "f": - self.f.append({ 'name': tokens[1], 'hash': tokens[2], 'size': tokens[3] }) - - def getVersion(self): - return self.version - - def getPath(self): - return self.path - - def getDirectories(self): - return self.d - - def getFiles(self): - return self.f - -################################################################################################################################# -class HTTPDownloadRequest(HTTPGetCallback): - def __init__(self, terrasync, src, dst, callback = None ): - super().__init__(src, self.callback) - self.terrasync = terrasync - self.dst = dst - self.mycallback = callback - - def callback(self): - if self.result.status != 200: - return - - with open(self.dst, 'wb') as f: - f.write(self.result.read()) - - if self.mycallback != None: - self.mycallback(self) - -################################################################################################################################# - -def hash_of_file(fname): - if not os.path.exists( fname ): - return None - - hash = hashlib.sha1() - try: - with open(fname, "rb") as f: - for chunk in iter(lambda: f.read(4096), b""): - hash.update(chunk) - except: - pass - - return hash.hexdigest() - -################################################################################################################################# - -class Coordinate: - def __init__(self, lat, lon): - self.lat = lat - self.lon = lon - -class DownloadBoundaries: - def __init__(self, top, left, bottom, right): - if top < bottom: - raise ValueError("top cannot be less than bottom") - if right < left: - raise ValueError("right cannot be less than left") - - if top > 90 or bottom < -90: - raise ValueError("top and bottom must be a valid latitude") - if left < -180 or right > 180: - raise ValueError("left and right must be a valid longitude") - self.top = top - self.left = left - self.bottom = bottom - self.right = right - - def is_coordinate_inside_boundaries(self, coordinate): - if coordinate.lat < self.bottom or coordinate.lat > self.top: - return False - if coordinate.lon < self.left or coordinate.lon > self.right: - return False - return True - -def parse_terrasync_coordinate(coordinate): - matches = re.match("(w|e)(\d{3})(n|s)(\d{2})", coordinate) - if not matches: - return None - lon = int(matches.group(2)) - if matches.group(1) == "w": - lon *= -1 - lat = int(matches.group(4)) - if matches.group(3) == "s": - lat *= -1 - return Coordinate(lat, lon) - -class TerraSync: - - def __init__(self, url, target, quick, removeOrphan, downloadBoundaries): - self.setUrl(url).setTarget(target) - self.quick = quick - self.removeOrphan = removeOrphan - self.httpGetter = None - self.downloadBoundaries = downloadBoundaries - - def setUrl(self, url): - self.url = url.rstrip('/').strip() - return self - - def setTarget(self, target): - self.target = target.rstrip('/').strip() - return self - - def start(self): - self.httpGetter = HTTPGetter(self.url) - self.updateDirectory("", "", None ) - - def updateFile(self, serverPath, localPath, fileHash ): - localFullPath = join(self.target, localPath) - if fileHash != None and hash_of_file(localFullPath) == fileHash: - #print("hash of file matches, not downloading") - return - - print("downloading ", serverPath ) - - request = HTTPDownloadRequest(self, serverPath, localFullPath ) - self.httpGetter.get(request) - - - def updateDirectory(self, serverPath, localPath, dirIndexHash): - print("processing ", serverPath) - - if len(serverPath) > 0: - serverFolderName = serverPath[serverPath.rfind('/') + 1:] - coordinate = parse_terrasync_coordinate(serverFolderName) - if coordinate and not self.downloadBoundaries.is_coordinate_inside_boundaries(coordinate): - return - - localFullPath = join(self.target, localPath) - if not os.path.exists( localFullPath ): - os.makedirs( localFullPath ) - - localDirIndex = join(localFullPath, ".dirindex") - if dirIndexHash != None and hash_of_file(localDirIndex) == dirIndexHash: - # print("hash of dirindex matches, not downloading") - if not self.quick: - self.handleDirindexFile( localDirIndex ) - else: - request = HTTPDownloadRequest(self, serverPath + "/.dirindex", localDirIndex, self.handleDirindexRequest ) - self.httpGetter.get(request) - - def handleDirindexRequest(self, dirindexRequest): - self.handleDirindexFile(dirindexRequest.dst) - - def handleDirindexFile(self, dirindexFile): - dirIndex = DirIndex(dirindexFile) - serverFiles = [] - serverDirs = [] - - for file in dirIndex.getFiles(): - f = file['name'] - h = file['hash'] - self.updateFile( "/" + dirIndex.getPath() + "/" + f, join(dirIndex.getPath(),f), h ) - serverFiles.append(f) - - for subdir in dirIndex.getDirectories(): - d = subdir['name'] - h = subdir['hash'] - self.updateDirectory( "/" + dirIndex.getPath() + "/" + d, join(dirIndex.getPath(),d), h ) - serverDirs.append(d) - - if self.removeOrphan: - localFullPath = join(self.target, dirIndex.getPath()) - localFiles = [f for f in listdir(localFullPath) if isfile(join(localFullPath, f))] - for f in localFiles: - if f != ".dirindex" and not f in serverFiles: - #print("removing orphan file", join(localFullPath,f) ) - os.remove( join(localFullPath,f) ) - localDirs = [f for f in listdir(localFullPath) if isdir(join(localFullPath, f))] - for f in localDirs: - if not f in serverDirs: - #print ("removing orphan dir",f) - shutil.rmtree( join(localFullPath,f) ) - - - def isReady(self): - return self.httpGetter and self.httpGetter.isReady() - return False - - def update(self): - if self.httpGetter: - self.httpGetter.update() - -################################################################################################################################# - - -parser = argparse.ArgumentParser() -parser.add_argument("-u", "--url", dest="url", metavar="URL", - default="http://flightgear.sourceforge.net/scenery", help="Server URL [default: %(default)s]") -parser.add_argument("-t", "--target", dest="target", metavar="DIR", - default=".", help="Directory to store the files [default: current directory]") -parser.add_argument("-q", "--quick", dest="quick", action="store_true", - default=False, help="Quick") -parser.add_argument("-r", "--remove-orphan", dest="removeOrphan", action="store_true", - default=False, help="Remove old scenery files") - -parser.add_argument("--top", dest="top", type=int, - default=90, help="Maximum latitude to include in download [default: %(default)d]") -parser.add_argument("--bottom", dest="bottom", type=int, - default=-90, help="Minimum latitude to include in download [default: %(default)d]") -parser.add_argument("--left", dest="left", type=int, - default=-180, help="Minimum longitude to include in download [default: %(default)d]") -parser.add_argument("--right", dest="right", type=int, - default=180, help="Maximum longitude to include in download [default: %(default)d]") - -args = parser.parse_args() - -terraSync = TerraSync(args.url, args.target, args.quick, args.removeOrphan, - DownloadBoundaries(args.top, args.left, args.bottom, args.right)) - -terraSync.start() diff -Nru flightgear-2017.3.1+dfsg/src/Add-ons/Addon.cxx flightgear-2018.1.1+dfsg/src/Add-ons/Addon.cxx --- flightgear-2017.3.1+dfsg/src/Add-ons/Addon.cxx 1970-01-01 00:00:00.000000000 +0000 +++ flightgear-2018.1.1+dfsg/src/Add-ons/Addon.cxx 2018-04-06 19:43:33.000000000 +0000 @@ -0,0 +1,352 @@ +// -*- coding: utf-8 -*- +// +// Addon.cxx --- FlightGear class holding add-on metadata +// Copyright (C) 2017, 2018 Florent Rougon +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include
+#include + +#include "addon_fwd.hxx" +#include "Addon.hxx" +#include "AddonMetadataParser.hxx" +#include "AddonVersion.hxx" +#include "exceptions.hxx" +#include "pointer_traits.hxx" + +namespace strutils = simgear::strutils; + +using std::string; +using std::vector; + +namespace flightgear +{ + +namespace addons +{ + +// *************************************************************************** +// * QualifiedUrl * +// *************************************************************************** + +QualifiedUrl::QualifiedUrl(UrlType type, string url, string detail) + : _type(type), + _url(std::move(url)), + _detail(std::move(detail)) +{ } + +UrlType QualifiedUrl::getType() const +{ return _type; } + +void QualifiedUrl::setType(UrlType type) +{ _type = type; } + +std::string QualifiedUrl::getUrl() const +{ return _url; } + +void QualifiedUrl::setUrl(const std::string& url) +{ _url = url; } + +std::string QualifiedUrl::getDetail() const +{ return _detail; } + +void QualifiedUrl::setDetail(const std::string& detail) +{ _detail = detail; } + +// *************************************************************************** +// * Addon * +// *************************************************************************** + +Addon::Addon(std::string id, AddonVersion version, SGPath basePath, + std::string minFGVersionRequired, std::string maxFGVersionRequired, + SGPropertyNode* addonNode) + : _id(std::move(id)), + _version( + shared_ptr_traits::makeStrongRef(std::move(version))), + _basePath(std::move(basePath)), + _minFGVersionRequired(std::move(minFGVersionRequired)), + _maxFGVersionRequired(std::move(maxFGVersionRequired)), + _addonNode(addonNode) +{ + if (_minFGVersionRequired.empty()) { + // This add-on metadata class appeared in FlightGear 2017.4.0 + _minFGVersionRequired = "2017.4.0"; + } + + if (_maxFGVersionRequired.empty()) { + _maxFGVersionRequired = "none"; // special value + } +} + +Addon::Addon() + : Addon(std::string(), AddonVersion(), SGPath(), std::string(), + std::string(), nullptr) +{ } + +std::string Addon::getId() const +{ return _id; } + +void Addon::setId(const std::string& addonId) +{ _id = addonId; } + +std::string Addon::getName() const +{ return _name; } + +void Addon::setName(const std::string& addonName) +{ _name = addonName; } + +AddonVersionRef Addon::getVersion() const +{ return _version; } + +void Addon::setVersion(const AddonVersion& addonVersion) +{ + using ptr_traits = shared_ptr_traits; + _version.reset(ptr_traits::makeStrongRef(addonVersion)); +} + +std::vector Addon::getAuthors() const +{ return _authors; } + +void Addon::setAuthors(const std::vector& addonAuthors) +{ _authors = addonAuthors; } + +std::vector Addon::getMaintainers() const +{ return _maintainers; } + +void Addon::setMaintainers(const std::vector& addonMaintainers) +{ _maintainers = addonMaintainers; } + +std::string Addon::getShortDescription() const +{ return _shortDescription; } + +void Addon::setShortDescription(const std::string& addonShortDescription) +{ _shortDescription = addonShortDescription; } + +std::string Addon::getLongDescription() const +{ return _longDescription; } + +void Addon::setLongDescription(const std::string& addonLongDescription) +{ _longDescription = addonLongDescription; } + +std::string Addon::getLicenseDesignation() const +{ return _licenseDesignation; } + +void Addon::setLicenseDesignation(const std::string& addonLicenseDesignation) +{ _licenseDesignation = addonLicenseDesignation; } + +SGPath Addon::getLicenseFile() const +{ return _licenseFile; } + +void Addon::setLicenseFile(const SGPath& addonLicenseFile) +{ _licenseFile = addonLicenseFile; } + +std::string Addon::getLicenseUrl() const +{ return _licenseUrl; } + +void Addon::setLicenseUrl(const std::string& addonLicenseUrl) +{ _licenseUrl = addonLicenseUrl; } + +std::vector Addon::getTags() const +{ return _tags; } + +void Addon::setTags(const std::vector& addonTags) +{ _tags = addonTags; } + +SGPath Addon::getBasePath() const +{ return _basePath; } + +void Addon::setBasePath(const SGPath& addonBasePath) +{ _basePath = addonBasePath; } + +std::string Addon::resourcePath(const std::string& relativePath) const +{ + if (strutils::starts_with(relativePath, "/")) { + throw errors::invalid_resource_path( + "addon-specific resource path '" + relativePath + "' shouldn't start " + "with a '/'"); + } + + return "[addon=" + getId() + "]" + relativePath; +} + +std::string Addon::getMinFGVersionRequired() const +{ return _minFGVersionRequired; } + +void Addon::setMinFGVersionRequired(const string& minFGVersionRequired) +{ _minFGVersionRequired = minFGVersionRequired; } + +std::string Addon::getMaxFGVersionRequired() const +{ return _maxFGVersionRequired; } + +void Addon::setMaxFGVersionRequired(const string& maxFGVersionRequired) +{ + if (maxFGVersionRequired.empty()) { + _maxFGVersionRequired = "none"; // special value + } else { + _maxFGVersionRequired = maxFGVersionRequired; + } +} + +std::string Addon::getHomePage() const +{ return _homePage; } + +void Addon::setHomePage(const std::string& addonHomePage) +{ _homePage = addonHomePage; } + +std::string Addon::getDownloadUrl() const +{ return _downloadUrl; } + +void Addon::setDownloadUrl(const std::string& addonDownloadUrl) +{ _downloadUrl = addonDownloadUrl; } + +std::string Addon::getSupportUrl() const +{ return _supportUrl; } + +void Addon::setSupportUrl(const std::string& addonSupportUrl) +{ _supportUrl = addonSupportUrl; } + +std::string Addon::getCodeRepositoryUrl() const +{ return _codeRepositoryUrl; } + +void Addon::setCodeRepositoryUrl(const std::string& addonCodeRepositoryUrl) +{ _codeRepositoryUrl = addonCodeRepositoryUrl; } + +std::string Addon::getTriggerProperty() const +{ return _triggerProperty; } + +void Addon::setTriggerProperty(const std::string& addonTriggerProperty) +{ _triggerProperty = addonTriggerProperty; } + +SGPropertyNode_ptr Addon::getAddonNode() const +{ return _addonNode; } + +void Addon::setAddonNode(SGPropertyNode* addonNode) +{ _addonNode = SGPropertyNode_ptr(addonNode); } + +naRef Addon::getAddonPropsNode() const +{ + FGNasalSys* nas = globals->get_subsystem(); + return nas->wrappedPropsNode(_addonNode.get()); +} + +SGPropertyNode_ptr Addon::getLoadedFlagNode() const +{ + return { _addonNode->getChild("loaded", 0, 1) }; +} + +int Addon::getLoadSequenceNumber() const +{ return _loadSequenceNumber; } + +void Addon::setLoadSequenceNumber(int num) +{ _loadSequenceNumber = num; } + +std::string Addon::str() const +{ + std::ostringstream oss; + oss << "addon '" << _id << "' (version = " << *_version + << ", base path = '" << _basePath.utf8Str() + << "', minFGVersionRequired = '" << _minFGVersionRequired + << "', maxFGVersionRequired = '" << _maxFGVersionRequired << "')"; + + return oss.str(); +} + +// Static method +SGPath Addon::getMetadataFile(const SGPath& addonPath) +{ + return MetadataParser::getMetadataFile(addonPath); +} + +SGPath Addon::getMetadataFile() const +{ + return getMetadataFile(getBasePath()); +} + +// Static method +Addon Addon::fromAddonDir(const SGPath& addonPath) +{ + Addon::Metadata metadata = MetadataParser::parseMetadataFile(addonPath); + + // Object holding all the add-on metadata + Addon addon{std::move(metadata.id), std::move(metadata.version), addonPath, + std::move(metadata.minFGVersionRequired), + std::move(metadata.maxFGVersionRequired)}; + addon.setName(std::move(metadata.name)); + addon.setAuthors(std::move(metadata.authors)); + addon.setMaintainers(std::move(metadata.maintainers)); + addon.setShortDescription(std::move(metadata.shortDescription)); + addon.setLongDescription(std::move(metadata.longDescription)); + addon.setLicenseDesignation(std::move(metadata.licenseDesignation)); + addon.setLicenseFile(std::move(metadata.licenseFile)); + addon.setLicenseUrl(std::move(metadata.licenseUrl)); + addon.setTags(std::move(metadata.tags)); + addon.setHomePage(std::move(metadata.homePage)); + addon.setDownloadUrl(std::move(metadata.downloadUrl)); + addon.setSupportUrl(std::move(metadata.supportUrl)); + addon.setCodeRepositoryUrl(std::move(metadata.codeRepositoryUrl)); + + return addon; +} + +// Static method +void Addon::setupGhost(nasal::Hash& addonsModule) +{ + nasal::Ghost::init("addons.Addon") + .member("id", &Addon::getId) + .member("name", &Addon::getName) + .member("version", &Addon::getVersion) + .member("authors", &Addon::getAuthors) + .member("maintainers", &Addon::getMaintainers) + .member("shortDescription", &Addon::getShortDescription) + .member("longDescription", &Addon::getLongDescription) + .member("licenseDesignation", &Addon::getLicenseDesignation) + .member("licenseFile", &Addon::getLicenseFile) + .member("licenseUrl", &Addon::getLicenseUrl) + .member("tags", &Addon::getTags) + .member("basePath", &Addon::getBasePath) + .method("resourcePath", &Addon::resourcePath) + .member("minFGVersionRequired", &Addon::getMinFGVersionRequired) + .member("maxFGVersionRequired", &Addon::getMaxFGVersionRequired) + .member("homePage", &Addon::getHomePage) + .member("downloadUrl", &Addon::getDownloadUrl) + .member("supportUrl", &Addon::getSupportUrl) + .member("codeRepositoryUrl", &Addon::getCodeRepositoryUrl) + .member("triggerProperty", &Addon::getTriggerProperty) + .member("node", &Addon::getAddonPropsNode) + .member("loadSequenceNumber", &Addon::getLoadSequenceNumber); +} + +std::ostream& operator<<(std::ostream& os, const Addon& addon) +{ + return os << addon.str(); +} + +} // of namespace addons + +} // of namespace flightgear diff -Nru flightgear-2017.3.1+dfsg/src/Add-ons/addon_fwd.hxx flightgear-2018.1.1+dfsg/src/Add-ons/addon_fwd.hxx --- flightgear-2017.3.1+dfsg/src/Add-ons/addon_fwd.hxx 1970-01-01 00:00:00.000000000 +0000 +++ flightgear-2018.1.1+dfsg/src/Add-ons/addon_fwd.hxx 2018-04-06 19:43:33.000000000 +0000 @@ -0,0 +1,70 @@ +// -*- coding: utf-8 -*- +// +// addon_fwd.hxx --- Forward declarations for the FlightGear add-on +// infrastructure +// Copyright (C) 2017 Florent Rougon +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +#ifndef FG_ADDON_FWD_HXX +#define FG_ADDON_FWD_HXX + +#include + +namespace flightgear +{ + +namespace addons +{ + +class Addon; +class AddonManager; +class AddonVersion; +class AddonVersionSuffix; +class ResourceProvider; + +enum class UrlType; +class QualifiedUrl; + +enum class ContactType; +class Contact; +class Author; +class Maintainer; + +using AddonRef = SGSharedPtr; +using AddonVersionRef = SGSharedPtr; +using ContactRef = SGSharedPtr; +using AuthorRef = SGSharedPtr; +using MaintainerRef = SGSharedPtr; + +namespace errors +{ + +class error; +class error_loading_config_file; +class no_metadata_file_found; +class error_loading_metadata_file; +class duplicate_registration_attempt; +class fg_version_too_old; +class fg_version_too_recent; +class invalid_resource_path; + +} // of namespace errors + +} // of namespace addons + +} // of namespace flightgear + +#endif // of FG_ADDON_FWD_HXX diff -Nru flightgear-2017.3.1+dfsg/src/Add-ons/Addon.hxx flightgear-2018.1.1+dfsg/src/Add-ons/Addon.hxx --- flightgear-2017.3.1+dfsg/src/Add-ons/Addon.hxx 1970-01-01 00:00:00.000000000 +0000 +++ flightgear-2018.1.1+dfsg/src/Add-ons/Addon.hxx 2018-04-06 19:43:33.000000000 +0000 @@ -0,0 +1,237 @@ +// -*- coding: utf-8 -*- +// +// Addon.hxx --- FlightGear class holding add-on metadata +// Copyright (C) 2017, 2018 Florent Rougon +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +#ifndef FG_ADDON_HXX +#define FG_ADDON_HXX + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "addon_fwd.hxx" +#include "contacts.hxx" +#include "AddonVersion.hxx" + +namespace flightgear +{ + +namespace addons +{ + +enum class UrlType { + author, + maintainer, + homePage, + download, + support, + codeRepository, + license +}; + +class QualifiedUrl +{ +public: + QualifiedUrl(UrlType type, std::string url, std::string detail = ""); + + UrlType getType() const; + void setType(UrlType type); + + std::string getUrl() const; + void setUrl(const std::string& url); + + std::string getDetail() const; + void setDetail(const std::string& detail); + +private: + UrlType _type; + std::string _url; + // Used to store the author or maintainer name when _type is UrlType::author + // or UrlType::maintainer. Could be used to record details about a website + // too (e.g., for a UrlType::support, something like “official forum”). + std::string _detail; +}; + +class Addon : public SGReferenced +{ +public: + // Default constructor. 'minFGVersionRequired' is initialized to "2017.4.0" + // and 'maxFGVersionRequired' to "none". + Addon(); + // An empty value for 'minFGVersionRequired' is translated into "2017.4.0". + // An empty value for 'maxFGVersionRequired' is translated into "none". + Addon(std::string id, AddonVersion version, SGPath basePath, + std::string minFGVersionRequired = "", + std::string maxFGVersionRequired = "", + SGPropertyNode* addonNode = nullptr); + + // Parse the add-on metadata file inside 'addonPath' (as defined by + // getMetadataFile()) and return the corresponding Addon instance. + static Addon fromAddonDir(const SGPath& addonPath); + + std::string getId() const; + void setId(const std::string& addonId); + + std::string getName() const; + void setName(const std::string& addonName); + + AddonVersionRef getVersion() const; + void setVersion(const AddonVersion& addonVersion); + + std::vector getAuthors() const; + void setAuthors(const std::vector& addonAuthors); + + std::vector getMaintainers() const; + void setMaintainers(const std::vector& addonMaintainers); + + std::string getShortDescription() const; + void setShortDescription(const std::string& addonShortDescription); + + std::string getLongDescription() const; + void setLongDescription(const std::string& addonLongDescription); + + std::string getLicenseDesignation() const; + void setLicenseDesignation(const std::string& addonLicenseDesignation); + + SGPath getLicenseFile() const; + void setLicenseFile(const SGPath& addonLicenseFile); + + std::string getLicenseUrl() const; + void setLicenseUrl(const std::string& addonLicenseUrl); + + std::vector getTags() const; + void setTags(const std::vector& addonTags); + + SGPath getBasePath() const; + void setBasePath(const SGPath& addonBasePath); + + // Return a resource path suitable for use with the simgear::ResourceManager. + // 'relativePath' is relative to the add-on base path, and should not start + // with a '/'. + std::string resourcePath(const std::string& relativePath) const; + + // Should be valid for use with simgear::strutils::compare_versions() + std::string getMinFGVersionRequired() const; + void setMinFGVersionRequired(const std::string& minFGVersionRequired); + + // Should be valid for use with simgear::strutils::compare_versions(), + // except for the special value "none". + std::string getMaxFGVersionRequired() const; + void setMaxFGVersionRequired(const std::string& maxFGVersionRequired); + + std::string getHomePage() const; + void setHomePage(const std::string& addonHomePage); + + std::string getDownloadUrl() const; + void setDownloadUrl(const std::string& addonDownloadUrl); + + std::string getSupportUrl() const; + void setSupportUrl(const std::string& addonSupportUrl); + + std::string getCodeRepositoryUrl() const; + void setCodeRepositoryUrl(const std::string& addonCodeRepositoryUrl); + + std::string getTriggerProperty() const; + void setTriggerProperty(const std::string& addonTriggerProperty); + + // Node pertaining to the add-on in the Global Property Tree + SGPropertyNode_ptr getAddonNode() const; + void setAddonNode(SGPropertyNode* addonNode); + // For Nasal: result as a props.Node object + naRef getAddonPropsNode() const; + + // Property node indicating whether the add-on is fully loaded + SGPropertyNode_ptr getLoadedFlagNode() const; + + // 0 for the first loaded add-on, 1 for the second, etc. + // -1 means “not set” (as done by the default constructor) + int getLoadSequenceNumber() const; + void setLoadSequenceNumber(int num); + + // Get all non-empty URLs pertaining to this add-on + std::multimap getUrls() const; + + // Simple string representation + std::string str() const; + + static void setupGhost(nasal::Hash& addonsModule); + +private: + class Metadata; + class MetadataParser; + + // “Compute” a path to the metadata file from the add-on base path + static SGPath getMetadataFile(const SGPath& addonPath); + SGPath getMetadataFile() const; + + // The add-on identifier, in reverse DNS style. The AddonManager refuses to + // register two add-ons with the same id in a given FlightGear session. + std::string _id; + // Pretty name for the add-on (not constrained to reverse DNS style) + std::string _name; + // Use a smart pointer to expose the AddonVersion instance to Nasal without + // needing to copy the data every time. + AddonVersionRef _version; + + std::vector _authors; + std::vector _maintainers; + + // Strings describing what the add-on does + std::string _shortDescription; + std::string _longDescription; + + std::string _licenseDesignation; + SGPath _licenseFile; + std::string _licenseUrl; + + std::vector _tags; + SGPath _basePath; + + // To be used with simgear::strutils::compare_versions() + std::string _minFGVersionRequired; + // Ditto, but there is a special value: "none" + std::string _maxFGVersionRequired; + + std::string _homePage; + std::string _downloadUrl; + std::string _supportUrl; + std::string _codeRepositoryUrl; + + // Main node for the add-on in the Property Tree + SGPropertyNode_ptr _addonNode; + // The add-on will be loaded when the property referenced by + // _triggerProperty is written to. + std::string _triggerProperty; + // Semantics explained above + int _loadSequenceNumber = -1; +}; + +std::ostream& operator<<(std::ostream& os, const Addon& addon); + +} // of namespace addons + +} // of namespace flightgear + +#endif // of FG_ADDON_HXX diff -Nru flightgear-2017.3.1+dfsg/src/Add-ons/AddonManager.cxx flightgear-2018.1.1+dfsg/src/Add-ons/AddonManager.cxx --- flightgear-2017.3.1+dfsg/src/Add-ons/AddonManager.cxx 1970-01-01 00:00:00.000000000 +0000 +++ flightgear-2018.1.1+dfsg/src/Add-ons/AddonManager.cxx 2018-04-06 19:43:33.000000000 +0000 @@ -0,0 +1,260 @@ +// -*- coding: utf-8 -*- +// +// AddonManager.cxx --- Manager class for FlightGear add-ons +// Copyright (C) 2017 Florent Rougon +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include
+#include
+ +#include "addon_fwd.hxx" +#include "Addon.hxx" +#include "AddonManager.hxx" +#include "AddonVersion.hxx" +#include "exceptions.hxx" +#include "pointer_traits.hxx" + +namespace strutils = simgear::strutils; + +using std::string; +using std::vector; +using std::shared_ptr; +using std::unique_ptr; + +namespace flightgear +{ + +namespace addons +{ + +static unique_ptr staticInstance; + +// *************************************************************************** +// * AddonManager * +// *************************************************************************** + +// Static method +const unique_ptr& +AddonManager::createInstance() +{ + SG_LOG(SG_GENERAL, SG_DEBUG, "Initializing the AddonManager"); + + staticInstance.reset(new AddonManager()); + return staticInstance; +} + +// Static method +const unique_ptr& +AddonManager::instance() +{ + return staticInstance; +} + +// Static method +void +AddonManager::reset() +{ + SG_LOG(SG_GENERAL, SG_DEBUG, "Resetting the AddonManager"); + staticInstance.reset(); +} + +// Static method +void +AddonManager::loadConfigFileIfExists(const SGPath& configFile) +{ + if (!configFile.exists()) { + return; + } + + try { + readProperties(configFile, globals->get_props()); + } catch (const sg_exception &e) { + throw errors::error_loading_config_file( + "unable to load add-on config file '" + configFile.utf8Str() + "': " + + e.getFormattedMessage()); + } + + SG_LOG(SG_GENERAL, SG_INFO, + "Loaded add-on config file: '" << configFile.utf8Str() + "'"); +} + +string +AddonManager::registerAddonMetadata(const SGPath& addonPath) +{ + using ptr_traits = shared_ptr_traits; + AddonRef addon = ptr_traits::makeStrongRef(Addon::fromAddonDir(addonPath)); + string addonId = addon->getId(); + + SGPropertyNode* addonPropNode = fgGetNode("addons", true) + ->getChild("by-id", 0, 1) + ->getChild(addonId, 0, 1); + addon->setAddonNode(addonPropNode); + addon->setLoadSequenceNumber(_loadSequenceNumber++); + + // Check that the FlightGear version satisfies the add-on requirements + std::string minFGversion = addon->getMinFGVersionRequired(); + if (strutils::compare_versions(FLIGHTGEAR_VERSION, minFGversion) < 0) { + throw errors::fg_version_too_old( + "add-on '" + addonId + "' requires FlightGear " + minFGversion + + " or later, however this is FlightGear " + FLIGHTGEAR_VERSION); + } + + std::string maxFGversion = addon->getMaxFGVersionRequired(); + if (maxFGversion != "none" && + strutils::compare_versions(FLIGHTGEAR_VERSION, maxFGversion) > 0) { + throw errors::fg_version_too_recent( + "add-on '" + addonId + "' requires FlightGear " + maxFGversion + + " or earlier, however this is FlightGear " + FLIGHTGEAR_VERSION); + } + + // Store the add-on metadata in _idToAddonMap + auto emplaceRetval = _idToAddonMap.emplace(addonId, std::move(addon)); + + // Prevent registration of two add-ons with the same id + if (!emplaceRetval.second) { + auto existingElt = _idToAddonMap.find(addonId); + assert(existingElt != _idToAddonMap.end()); + throw errors::duplicate_registration_attempt( + "attempt to register add-on '" + addonId + "' with base path '" + + addonPath.utf8Str() + "', however it is already registered with base " + "path '" + existingElt->second->getBasePath().utf8Str() + "'"); + } + + return addonId; +} + +string +AddonManager::registerAddon(const SGPath& addonPath) +{ + // Use realpath() as in FGGlobals::append_aircraft_path(), otherwise + // fgValidatePath() will deny access to resources under the add-on path if + // one of its components is a symlink. + const SGPath addonRealPath = addonPath.realpath(); + const string addonId = registerAddonMetadata(addonRealPath); + loadConfigFileIfExists(addonRealPath / "addon-config.xml"); + globals->append_aircraft_path(addonRealPath); + + AddonRef addon{getAddon(addonId)}; + addon->getLoadedFlagNode()->setBoolValue(false); + SGPropertyNode_ptr addonNode = addon->getAddonNode(); + + // Set a few properties for the add-on under this node + addonNode->getNode("id", true)->setStringValue(addonId); + addonNode->getNode("name", true)->setStringValue(addon->getName()); + addonNode->getNode("version", true) + ->setStringValue(addonVersion(addonId)->str()); + addonNode->getNode("path", true)->setStringValue(addonRealPath.utf8Str()); + addonNode->getNode("load-seq-num", true) + ->setIntValue(addon->getLoadSequenceNumber()); + + // “Legacy node”. Should we remove these two lines? + SGPropertyNode* seqNumNode = fgGetNode("addons", true)->addChild("addon"); + seqNumNode->getNode("path", true)->setStringValue(addonRealPath.utf8Str()); + + string msg = "Registered add-on '" + addon->getName() + "' (" + addonId + + ") version " + addonVersion(addonId)->str() + "; " + "base path is '" + addonRealPath.utf8Str() + "'"; + // This preserves the registration order + _registeredAddons.push_back(std::move(addon)); + SG_LOG(SG_GENERAL, SG_INFO, msg); + + return addonId; +} + +bool +AddonManager::isAddonRegistered(const string& addonId) const +{ + return (_idToAddonMap.find(addonId) != _idToAddonMap.end()); +} + +bool +AddonManager::isAddonLoaded(const string& addonId) const +{ + return (isAddonRegistered(addonId) && + getAddon(addonId)->getLoadedFlagNode()->getBoolValue()); +} + +vector +AddonManager::registeredAddons() const +{ + return _registeredAddons; +} + +vector +AddonManager::loadedAddons() const +{ + vector v; + v.reserve(_idToAddonMap.size()); // will be the right size most of the times + + for (const auto& elem: _idToAddonMap) { + if (isAddonLoaded(elem.first)) { + v.push_back(elem.second); + } + } + + return v; +} + +AddonRef +AddonManager::getAddon(const string& addonId) const +{ + const auto it = _idToAddonMap.find(addonId); + + if (it == _idToAddonMap.end()) { + throw sg_exception("tried to get add-on '" + addonId + "', however no " + "such add-on has been registered."); + } + + return it->second; +} + +AddonVersionRef +AddonManager::addonVersion(const string& addonId) const +{ + return getAddon(addonId)->getVersion(); +} + +SGPath +AddonManager::addonBasePath(const string& addonId) const +{ + return getAddon(addonId)->getBasePath(); +} + +SGPropertyNode_ptr AddonManager::addonNode(const string& addonId) const +{ + return getAddon(addonId)->getAddonNode(); +} + +} // of namespace addons + +} // of namespace flightgear diff -Nru flightgear-2017.3.1+dfsg/src/Add-ons/AddonManager.hxx flightgear-2018.1.1+dfsg/src/Add-ons/AddonManager.hxx --- flightgear-2017.3.1+dfsg/src/Add-ons/AddonManager.hxx 1970-01-01 00:00:00.000000000 +0000 +++ flightgear-2018.1.1+dfsg/src/Add-ons/AddonManager.hxx 2018-04-06 19:43:33.000000000 +0000 @@ -0,0 +1,110 @@ +// -*- coding: utf-8 -*- +// +// AddonManager.hxx --- Manager class for FlightGear add-ons +// Copyright (C) 2017 Florent Rougon +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +#ifndef FG_ADDONMANAGER_HXX +#define FG_ADDONMANAGER_HXX + +#include +#include +#include // std::unique_ptr, std::shared_ptr +#include + +#include +#include + +#include "addon_fwd.hxx" +#include "Addon.hxx" +#include "AddonVersion.hxx" + +namespace flightgear +{ + +namespace addons +{ + +class AddonManager +{ +public: + AddonManager(const AddonManager&) = delete; + AddonManager& operator=(const AddonManager&) = delete; + AddonManager(AddonManager&&) = delete; + AddonManager& operator=(AddonManager&&) = delete; + // The instance is created by createInstance() -> private constructor + // but it should be deleted by its owning std::unique_ptr -> public destructor + ~AddonManager() = default; + + // Static creator + static const std::unique_ptr& createInstance(); + // Singleton accessor + static const std::unique_ptr& instance(); + // Reset the static smart pointer, i.e., shut down the AddonManager. + static void reset(); + + // Register an add-on and return its id. + // 'addonPath': directory containing the add-on to register + // + // This comprises the following steps, where $path = addonPath.realpath(): + // - load add-on metadata from $path/addon-metadata.xml and register it + // inside _idToAddonMap (this step is done via registerAddonMetadata()); + // - load $path/addon-config.xml into the Property Tree; + // - append $path to the list of aircraft paths; + // - make part of the add-on metadata available in the Property Tree under + // the /addons node (/addons/by-id//{id,version,path,...}); + // - append a ref to the Addon instance to _registeredAddons. + std::string registerAddon(const SGPath& addonPath); + // Return the list of registered add-ons in registration order (which, BTW, + // is the same as load order). + std::vector registeredAddons() const; + bool isAddonRegistered(const std::string& addonId) const; + + // A loaded add-on is one whose addon-main.nas file has been loaded. The + // returned vector is sorted by add-on id (cheap sorting based on UTF-8 code + // units, only guaranteed correct for ASCII chars). + std::vector loadedAddons() const; + bool isAddonLoaded(const std::string& addonId) const; + + AddonRef getAddon(const std::string& addonId) const; + AddonVersionRef addonVersion(const std::string& addonId) const; + SGPath addonBasePath(const std::string& addonId) const; + + // Base node pertaining to the add-on in the Global Property Tree + SGPropertyNode_ptr addonNode(const std::string& addonId) const; + +private: + // Constructor called from createInstance() only + explicit AddonManager() = default; + + static void loadConfigFileIfExists(const SGPath& configFile); + // Register add-on metadata inside _idToAddonMap and return the add-on id + std::string registerAddonMetadata(const SGPath& addonPath); + + // Map each add-on id to the corresponding Addon instance. + std::map _idToAddonMap; + // The order in _registeredAddons is the registration order. + std::vector _registeredAddons; + // 0 for the first loaded add-on, 1 for the second, etc. + // Also note that add-ons are loaded in their registration order. + int _loadSequenceNumber = 0; +}; + +} // of namespace addons + +} // of namespace flightgear + +#endif // of FG_ADDONMANAGER_HXX diff -Nru flightgear-2017.3.1+dfsg/src/Add-ons/AddonMetadataParser.cxx flightgear-2018.1.1+dfsg/src/Add-ons/AddonMetadataParser.cxx --- flightgear-2017.3.1+dfsg/src/Add-ons/AddonMetadataParser.cxx 1970-01-01 00:00:00.000000000 +0000 +++ flightgear-2018.1.1+dfsg/src/Add-ons/AddonMetadataParser.cxx 2018-04-06 19:43:33.000000000 +0000 @@ -0,0 +1,380 @@ +// -*- coding: utf-8 -*- +// +// AddonMetadataParser.cxx --- Parser for FlightGear add-on metadata files +// Copyright (C) 2018 Florent Rougon +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "addon_fwd.hxx" +#include "AddonMetadataParser.hxx" +#include "AddonVersion.hxx" +#include "contacts.hxx" +#include "exceptions.hxx" +#include "pointer_traits.hxx" + +namespace strutils = simgear::strutils; + +using std::string; +using std::vector; + +namespace flightgear +{ + +namespace addons +{ + +// Static method +SGPath +Addon::MetadataParser::getMetadataFile(const SGPath& addonPath) +{ + return addonPath / "addon-metadata.xml"; +} + +// Static method +Addon::Metadata +Addon::MetadataParser::parseMetadataFile(const SGPath& addonPath) +{ + SGPath metadataFile = getMetadataFile(addonPath); + SGPropertyNode addonRoot; + Addon::Metadata metadata; + + if (!metadataFile.exists()) { + throw errors::no_metadata_file_found( + "unable to find add-on metadata file '" + metadataFile.utf8Str() + "'"); + } + + try { + readProperties(metadataFile, &addonRoot); + } catch (const sg_exception &e) { + throw errors::error_loading_metadata_file( + "unable to load add-on metadata file '" + metadataFile.utf8Str() + "': " + + e.getFormattedMessage()); + } + + // Check the 'meta' section + SGPropertyNode *metaNode = addonRoot.getChild("meta"); + if (metaNode == nullptr) { + throw errors::error_loading_metadata_file( + "no /meta node found in add-on metadata file '" + + metadataFile.utf8Str() + "'"); + } + + // Check the file type + SGPropertyNode *fileTypeNode = metaNode->getChild("file-type"); + if (fileTypeNode == nullptr) { + throw errors::error_loading_metadata_file( + "no /meta/file-type node found in add-on metadata file '" + + metadataFile.utf8Str() + "'"); + } + + string fileType = fileTypeNode->getStringValue(); + if (fileType != "FlightGear add-on metadata") { + throw errors::error_loading_metadata_file( + "Invalid /meta/file-type value for add-on metadata file '" + + metadataFile.utf8Str() + "': '" + fileType + "' " + "(expected 'FlightGear add-on metadata')"); + } + + // Check the format version + SGPropertyNode *fmtVersionNode = metaNode->getChild("format-version"); + if (fmtVersionNode == nullptr) { + throw errors::error_loading_metadata_file( + "no /meta/format-version node found in add-on metadata file '" + + metadataFile.utf8Str() + "'"); + } + + int formatVersion = fmtVersionNode->getIntValue(); + if (formatVersion != 1) { + throw errors::error_loading_metadata_file( + "unknown format version in add-on metadata file '" + + metadataFile.utf8Str() + "': " + std::to_string(formatVersion)); + } + + // Now the data we are really interested in + SGPropertyNode *addonNode = addonRoot.getChild("addon"); + if (addonNode == nullptr) { + throw errors::error_loading_metadata_file( + "no /addon node found in add-on metadata file '" + + metadataFile.utf8Str() + "'"); + } + + SGPropertyNode *idNode = addonNode->getChild("identifier"); + if (idNode == nullptr) { + throw errors::error_loading_metadata_file( + "no /addon/identifier node found in add-on metadata file '" + + metadataFile.utf8Str() + "'"); + } + metadata.id = strutils::strip(idNode->getStringValue()); + + // Require a non-empty identifier for the add-on + if (metadata.id.empty()) { + throw errors::error_loading_metadata_file( + "empty or whitespace-only value for the /addon/identifier node in " + "add-on metadata file '" + metadataFile.utf8Str() + "'"); + } else if (metadata.id.find('.') == string::npos) { + SG_LOG(SG_GENERAL, SG_WARN, + "Add-on identifier '" << metadata.id << "' does not use reverse DNS " + "style (e.g., org.flightgear.addons.MyAddon) in add-on metadata " + "file '" << metadataFile.utf8Str() + "'"); + } + + SGPropertyNode *nameNode = addonNode->getChild("name"); + if (nameNode == nullptr) { + throw errors::error_loading_metadata_file( + "no /addon/name node found in add-on metadata file '" + + metadataFile.utf8Str() + "'"); + } + metadata.name = strutils::strip(nameNode->getStringValue()); + + // Require a non-empty name for the add-on + if (metadata.name.empty()) { + throw errors::error_loading_metadata_file( + "empty or whitespace-only value for the /addon/name node in add-on " + "metadata file '" + metadataFile.utf8Str() + "'"); + } + + SGPropertyNode *versionNode = addonNode->getChild("version"); + if (versionNode == nullptr) { + throw errors::error_loading_metadata_file( + "no /addon/version node found in add-on metadata file '" + + metadataFile.utf8Str() + "'"); + } + metadata.version = AddonVersion{ + strutils::strip(versionNode->getStringValue())}; + + metadata.authors = parseContactsNode(metadataFile, + addonNode->getChild("authors")); + metadata.maintainers = parseContactsNode( + metadataFile, addonNode->getChild("maintainers")); + + SGPropertyNode *shortDescNode = addonNode->getChild("short-description"); + if (shortDescNode != nullptr) { + metadata.shortDescription = strutils::strip(shortDescNode->getStringValue()); + } else { + metadata.shortDescription = string(); + } + + SGPropertyNode *longDescNode = addonNode->getChild("long-description"); + if (longDescNode != nullptr) { + metadata.longDescription = strutils::strip(longDescNode->getStringValue()); + } else { + metadata.longDescription = string(); + } + + std::tie(metadata.licenseDesignation, metadata.licenseFile, + metadata.licenseUrl) = parseLicenseNode(addonPath, addonNode); + + SGPropertyNode *tagsNode = addonNode->getChild("tags"); + if (tagsNode != nullptr) { + auto tagNodes = tagsNode->getChildren("tag"); + for (const auto& node: tagNodes) { + metadata.tags.push_back(strutils::strip(node->getStringValue())); + } + } + + SGPropertyNode *minNode = addonNode->getChild("min-FG-version"); + if (minNode != nullptr) { + metadata.minFGVersionRequired = strutils::strip(minNode->getStringValue()); + } else { + metadata.minFGVersionRequired = string(); + } + + SGPropertyNode *maxNode = addonNode->getChild("max-FG-version"); + if (maxNode != nullptr) { + metadata.maxFGVersionRequired = strutils::strip(maxNode->getStringValue()); + } else { + metadata.maxFGVersionRequired = string(); + } + + metadata.homePage = metadata.downloadUrl = metadata.supportUrl = + metadata.codeRepositoryUrl = string(); // defaults + SGPropertyNode *urlsNode = addonNode->getChild("urls"); + if (urlsNode != nullptr) { + SGPropertyNode *homePageNode = urlsNode->getChild("home-page"); + if (homePageNode != nullptr) { + metadata.homePage = strutils::strip(homePageNode->getStringValue()); + } + + SGPropertyNode *downloadUrlNode = urlsNode->getChild("download"); + if (downloadUrlNode != nullptr) { + metadata.downloadUrl = strutils::strip(downloadUrlNode->getStringValue()); + } + + SGPropertyNode *supportUrlNode = urlsNode->getChild("support"); + if (supportUrlNode != nullptr) { + metadata.supportUrl = strutils::strip(supportUrlNode->getStringValue()); + } + + SGPropertyNode *codeRepoUrlNode = urlsNode->getChild("code-repository"); + if (codeRepoUrlNode != nullptr) { + metadata.codeRepositoryUrl = + strutils::strip(codeRepoUrlNode->getStringValue()); + } + } + + SG_LOG(SG_GENERAL, SG_DEBUG, + "Parsed add-on metadata file: '" << metadataFile.utf8Str() + "'"); + + return metadata; +} + +// Utility function for Addon::MetadataParser::parseContactsNode<>() +// +// Read a node such as "name", "email" or "url", child of a contact node (e.g., +// of an "author" or "maintainer" node). +static string +parseContactsNode_readNode(const SGPath& metadataFile, + SGPropertyNode* contactNode, + string subnodeName, bool allowEmpty) +{ + SGPropertyNode *node = contactNode->getChild(subnodeName); + string contents; + + if (node != nullptr) { + contents = simgear::strutils::strip(node->getStringValue()); + } + + if (!allowEmpty && contents.empty()) { + throw errors::error_loading_metadata_file( + "in add-on metadata file '" + metadataFile.utf8Str() + "': " + "when the node " + contactNode->getPath(true) + " exists, it must have " + "a non-empty '" + subnodeName + "' child node"); + } + + return contents; +}; + +// Static method template (private and only used in this file) +template +vector::strong_ref> +Addon::MetadataParser::parseContactsNode(const SGPath& metadataFile, + SGPropertyNode* mainNode) +{ + using contactTraits = contact_traits; + vector res; + + if (mainNode != nullptr) { + auto contactNodes = mainNode->getChildren(contactTraits::xmlNodeName()); + res.reserve(contactNodes.size()); + + for (const auto& contactNode: contactNodes) { + string name, email, url; + + name = parseContactsNode_readNode(metadataFile, contactNode.get(), + "name", false /* allowEmpty */); + email = parseContactsNode_readNode(metadataFile, contactNode.get(), + "email", true); + url = parseContactsNode_readNode(metadataFile, contactNode.get(), + "url", true); + + using ptr_traits = shared_ptr_traits; + res.push_back(ptr_traits::makeStrongRef(name, email, url)); + } + } + + return res; +}; + +// Static method +std::tuple +Addon::MetadataParser::parseLicenseNode(const SGPath& addonPath, + SGPropertyNode* addonNode) +{ + SGPath metadataFile = getMetadataFile(addonPath); + string licenseDesignation; + SGPath licenseFile; + string licenseUrl; + + SGPropertyNode *licenseNode = addonNode->getChild("license"); + if (licenseNode == nullptr) { + return std::tuple(); + } + + SGPropertyNode *licenseDesigNode = licenseNode->getChild("designation"); + if (licenseDesigNode != nullptr) { + licenseDesignation = strutils::strip(licenseDesigNode->getStringValue()); + } + + SGPropertyNode *licenseFileNode = licenseNode->getChild("file"); + if (licenseFileNode != nullptr) { + // This effectively disallows filenames starting or ending with whitespace + string licenseFile_s = strutils::strip(licenseFileNode->getStringValue()); + + if (!licenseFile_s.empty()) { + if (licenseFile_s.find('\\') != string::npos) { + throw errors::error_loading_metadata_file( + "in add-on metadata file '" + metadataFile.utf8Str() + "': the " + "value of /addon/license/file contains '\\'; please use '/' " + "separators only"); + } + + if (licenseFile_s.find_first_of("/\\") == 0) { + throw errors::error_loading_metadata_file( + "in add-on metadata file '" + metadataFile.utf8Str() + "': the " + "value of /addon/license/file must be relative to the add-on folder, " + "however it starts with '" + licenseFile_s[0] + "'"); + } + +#ifdef HAVE_WORKING_STD_REGEX + std::regex winDriveRegexp("([a-zA-Z]:).*"); + std::smatch results; + + if (std::regex_match(licenseFile_s, results, winDriveRegexp)) { + string winDrive = results.str(1); +#else // all this 'else' clause should be removed once we actually require C++11 + if (licenseFile_s.size() >= 2 && + (('a' <= licenseFile_s[0] && licenseFile_s[0] <= 'z') || + ('A' <= licenseFile_s[0] && licenseFile_s[0] <= 'Z')) && + licenseFile_s[1] == ':') { + string winDrive = licenseFile_s.substr(0, 2); +#endif + throw errors::error_loading_metadata_file( + "in add-on metadata file '" + metadataFile.utf8Str() + "': the " + "value of /addon/license/file must be relative to the add-on folder, " + "however it starts with a Windows drive letter (" + winDrive + ")"); + } + + licenseFile = addonPath / licenseFile_s; + if ( !(licenseFile.exists() && licenseFile.isFile()) ) { + throw errors::error_loading_metadata_file( + "in add-on metadata file '" + metadataFile.utf8Str() + "': the " + "value of /addon/license/file (pointing to '" + licenseFile.utf8Str() + + "') doesn't correspond to an existing file"); + } + } // of if (!licenseFile_s.empty()) + } // of if (licenseFileNode != nullptr) + + SGPropertyNode *licenseUrlNode = licenseNode->getChild("url"); + if (licenseUrlNode != nullptr) { + licenseUrl = strutils::strip(licenseUrlNode->getStringValue()); + } + + return std::make_tuple(licenseDesignation, licenseFile, licenseUrl); +} + +} // of namespace addons + +} // of namespace flightgear diff -Nru flightgear-2017.3.1+dfsg/src/Add-ons/AddonMetadataParser.hxx flightgear-2018.1.1+dfsg/src/Add-ons/AddonMetadataParser.hxx --- flightgear-2017.3.1+dfsg/src/Add-ons/AddonMetadataParser.hxx 1970-01-01 00:00:00.000000000 +0000 +++ flightgear-2018.1.1+dfsg/src/Add-ons/AddonMetadataParser.hxx 2018-04-06 19:43:33.000000000 +0000 @@ -0,0 +1,97 @@ +// -*- coding: utf-8 -*- +// +// AddonMetadataParser.hxx --- Parser for FlightGear add-on metadata files +// Copyright (C) 2018 Florent Rougon +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +#ifndef FG_ADDON_METADATA_PARSER_HXX +#define FG_ADDON_METADATA_PARSER_HXX + +#include +#include +#include + +#include + +#include "addon_fwd.hxx" +#include "Addon.hxx" +#include "AddonVersion.hxx" +#include "contacts.hxx" + +class SGPropertyNode; + +namespace flightgear +{ + +namespace addons +{ + +class Addon::Metadata +{ +public: + // Comments about these fields can be found in Addon.hxx + std::string id; + std::string name; + AddonVersion version; + + std::vector authors; + std::vector maintainers; + + std::string shortDescription; + std::string longDescription; + + std::string licenseDesignation; + SGPath licenseFile; + std::string licenseUrl; + + std::vector tags; + + std::string minFGVersionRequired; + std::string maxFGVersionRequired; + + std::string homePage; + std::string downloadUrl; + std::string supportUrl; + std::string codeRepositoryUrl; +}; + +class Addon::MetadataParser +{ +public: + // “Compute” a path to the metadata file from the add-on base path + static SGPath getMetadataFile(const SGPath& addonPath); + + // Parse the add-on metadata file inside 'addonPath' (as defined by + // getMetadataFile()) and return the corresponding Addon::Metadata instance. + static Addon::Metadata parseMetadataFile(const SGPath& addonPath); + +private: + static std::tuple + parseLicenseNode(const SGPath& addonPath, SGPropertyNode* addonNode); + + // Parse an addon-metadata.xml node such as or . + // Return the corresponding vector or vector. If + // the 'mainNode' argument is nullptr, return an empty vector. + template + static std::vector::strong_ref> + parseContactsNode(const SGPath& metadataFile, SGPropertyNode* mainNode); +}; + +} // of namespace addons + +} // of namespace flightgear + +#endif // of FG_ADDON_METADATA_PARSER_HXX diff -Nru flightgear-2017.3.1+dfsg/src/Add-ons/AddonResourceProvider.cxx flightgear-2018.1.1+dfsg/src/Add-ons/AddonResourceProvider.cxx --- flightgear-2017.3.1+dfsg/src/Add-ons/AddonResourceProvider.cxx 1970-01-01 00:00:00.000000000 +0000 +++ flightgear-2018.1.1+dfsg/src/Add-ons/AddonResourceProvider.cxx 2018-04-06 19:43:33.000000000 +0000 @@ -0,0 +1,80 @@ +// -*- coding: utf-8 -*- +// +// AddonResourceProvider.cxx --- ResourceProvider subclass for add-on files +// Copyright (C) 2018 Florent Rougon +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +#include + +#include +#include +#include + +#include
+ +#include "AddonManager.hxx" +#include "AddonResourceProvider.hxx" + +namespace strutils = simgear::strutils; + +using std::string; + +namespace flightgear +{ + +namespace addons +{ + +ResourceProvider::ResourceProvider() + : simgear::ResourceProvider(simgear::ResourceManager::PRIORITY_DEFAULT) +{ } + +SGPath +ResourceProvider::resolve(const string& resource, SGPath& context) const +{ + if (!strutils::starts_with(resource, "[addon=")) { + return SGPath(); + } + + string rest = resource.substr(7); // what follows '[addon=' + auto endOfAddonId = rest.find(']'); + + if (endOfAddonId == string::npos) { + return SGPath(); + } + + string addonId = rest.substr(0, endOfAddonId); + // Extract what follows '[addon=ADDON_ID]' + string relPath = rest.substr(endOfAddonId + 1); + + if (relPath.empty()) { + return SGPath(); + } + + const auto& addonMgr = AddonManager::instance(); + SGPath addonDir = addonMgr->addonBasePath(addonId); + SGPath candidate = addonDir / relPath; + + if (!candidate.isFile()) { + return SGPath(); + } + + return fgValidatePath(candidate, /* write */ false); +} + +} // of namespace addons + +} // of namespace flightgear diff -Nru flightgear-2017.3.1+dfsg/src/Add-ons/AddonResourceProvider.hxx flightgear-2018.1.1+dfsg/src/Add-ons/AddonResourceProvider.hxx --- flightgear-2017.3.1+dfsg/src/Add-ons/AddonResourceProvider.hxx 1970-01-01 00:00:00.000000000 +0000 +++ flightgear-2018.1.1+dfsg/src/Add-ons/AddonResourceProvider.hxx 2018-04-06 19:43:33.000000000 +0000 @@ -0,0 +1,47 @@ +// -*- coding: utf-8 -*- +// +// AddonResourceProvider.hxx --- ResourceProvider subclass for add-on files +// Copyright (C) 2018 Florent Rougon +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +#ifndef FG_ADDON_RESOURCE_PROVIDER_HXX +#define FG_ADDON_RESOURCE_PROVIDER_HXX + +#include + +#include +#include + +namespace flightgear +{ + +namespace addons +{ + +class ResourceProvider : public simgear::ResourceProvider +{ +public: + ResourceProvider(); + + virtual SGPath resolve(const std::string& resource, SGPath& context) const + override; +}; + +} // of namespace addons + +} // of namespace flightgear + +#endif // of FG_ADDON_RESOURCE_PROVIDER_HXX diff -Nru flightgear-2017.3.1+dfsg/src/Add-ons/AddonVersion.cxx flightgear-2018.1.1+dfsg/src/Add-ons/AddonVersion.cxx --- flightgear-2017.3.1+dfsg/src/Add-ons/AddonVersion.cxx 1970-01-01 00:00:00.000000000 +0000 +++ flightgear-2018.1.1+dfsg/src/Add-ons/AddonVersion.cxx 2018-04-06 19:43:33.000000000 +0000 @@ -0,0 +1,569 @@ +// -*- coding: utf-8 -*- +// +// AddonVersion.cxx --- Version class for FlightGear add-ons +// Copyright (C) 2017 Florent Rougon +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +#include // std::accumulate() +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include + +#include "addon_fwd.hxx" +#include "AddonVersion.hxx" + +using std::string; +using std::vector; +using simgear::enumValue; + +namespace strutils = simgear::strutils; + +namespace flightgear +{ + +namespace addons +{ + +// *************************************************************************** +// * AddonVersionSuffix * +// *************************************************************************** + +AddonVersionSuffix::AddonVersionSuffix( + AddonVersionSuffixPrereleaseType preReleaseType, int preReleaseNum, + bool developmental, int devNum) + : _preReleaseType(preReleaseType), + _preReleaseNum(preReleaseNum), + _developmental(developmental), + _devNum(devNum) +{ } + +// Construct an AddonVersionSuffix instance from a tuple (preReleaseType, +// preReleaseNum, developmental, devNum). This would be nicer with +// std::apply(), but it requires C++17. +AddonVersionSuffix::AddonVersionSuffix( + const std::tuple& t) + : AddonVersionSuffix(std::get<0>(t), std::get<1>(t), std::get<2>(t), + std::get<3>(t)) +{ } + +AddonVersionSuffix::AddonVersionSuffix(const std::string& suffix) + : AddonVersionSuffix(suffixStringToTuple(suffix)) +{ } + +AddonVersionSuffix::AddonVersionSuffix(const char* suffix) + : AddonVersionSuffix(string(suffix)) +{ } + +// Static method +string +AddonVersionSuffix::releaseTypeStr(AddonVersionSuffixPrereleaseType releaseType) +{ + switch (releaseType) { + case AddonVersionSuffixPrereleaseType::alpha: + return string("a"); + case AddonVersionSuffixPrereleaseType::beta: + return string("b"); + case AddonVersionSuffixPrereleaseType::candidate: + return string("rc"); + case AddonVersionSuffixPrereleaseType::none: + return string(); + default: + throw sg_error("unexpected value for member of " + "flightgear::addons::AddonVersionSuffixPrereleaseType: " + + std::to_string(enumValue(releaseType))); + } +} + +string +AddonVersionSuffix::str() const +{ + string res = releaseTypeStr(_preReleaseType); + + if (!res.empty()) { + res += std::to_string(_preReleaseNum); + } + + if (_developmental) { + res += ".dev" + std::to_string(_devNum); + } + + return res; +} + +// Static method +std::tuple +AddonVersionSuffix::suffixStringToTuple(const std::string& suffix) +{ +#ifdef HAVE_WORKING_STD_REGEX + // Use a simplified variant of the syntax described in PEP 440 + // : for the version suffix, only + // allow a pre-release segment and a development release segment, but no + // post-release segment. + std::regex versionSuffixRegexp(R"((?:(a|b|rc)(\d+))?(?:\.dev(\d+))?)"); + std::smatch results; + + if (std::regex_match(suffix, results, versionSuffixRegexp)) { + const string preReleaseType_s = results.str(1); + const string preReleaseNum_s = results.str(2); + const string devNum_s = results.str(3); + + AddonVersionSuffixPrereleaseType preReleaseType; + int preReleaseNum = 0; + int devNum = 0; + + if (preReleaseType_s.empty()) { + preReleaseType = AddonVersionSuffixPrereleaseType::none; + } else { + if (preReleaseType_s == "a") { + preReleaseType = AddonVersionSuffixPrereleaseType::alpha; + } else if (preReleaseType_s == "b") { + preReleaseType = AddonVersionSuffixPrereleaseType::beta; + } else if (preReleaseType_s == "rc") { + preReleaseType = AddonVersionSuffixPrereleaseType::candidate; + } else { + assert(false); // the regexp should prevent this + } + + assert(!preReleaseNum_s.empty()); + preReleaseNum = strutils::readNonNegativeInt(preReleaseNum_s); + + if (preReleaseNum < 1) { + string msg = "invalid add-on version suffix: '" + suffix + "' " + "(prerelease number must be greater than or equal to 1, but got " + + preReleaseNum_s + ")"; + throw sg_format_exception(msg, suffix); + } + } + + if (!devNum_s.empty()) { + devNum = strutils::readNonNegativeInt(devNum_s); + + if (devNum < 1) { + string msg = "invalid add-on version suffix: '" + suffix + "' " + "(development release number must be greater than or equal to 1, " + "but got " + devNum_s + ")"; + throw sg_format_exception(msg, suffix); + } + } + + return std::make_tuple(preReleaseType, preReleaseNum, !devNum_s.empty(), + devNum); +#else // all this 'else' clause should be removed once we actually require C++11 + bool isMatch; + AddonVersionSuffixPrereleaseType preReleaseType; + int preReleaseNum; + bool developmental; + int devNum; + + std::tie(isMatch, preReleaseType, preReleaseNum, developmental, devNum) = + parseVersionSuffixString_noRegexp(suffix); + + if (isMatch) { + return std::make_tuple(preReleaseType, preReleaseNum, developmental, + devNum); +#endif // HAVE_WORKING_STD_REGEX + } else { // the regexp didn't match + string msg = "invalid add-on version suffix: '" + suffix + "' " + "(expected form is [{a|b|rc}N1][.devN2] where N1 and N2 are positive " + "integers)"; + throw sg_format_exception(msg, suffix); + } +} + +// Static method, only needed for compilers that are not C++11-compliant +// (gcc 4.8 pretends to support as required by C++11 but doesn't, see +// ). +std::tuple +AddonVersionSuffix::parseVersionSuffixString_noRegexp(const string& suffix) +{ + AddonVersionSuffixPrereleaseType preReleaseType; + string rest; + int preReleaseNum = 0; // alpha, beta or release candidate number, or + // 0 when absent + bool developmental = false; // whether 'suffix' has a .devN2 part + int devNum = 0; // the N2 in question, or 0 when absent + + std::tie(preReleaseType, rest) = popPrereleaseTypeFromBeginning(suffix); + + if (preReleaseType != AddonVersionSuffixPrereleaseType::none) { + std::size_t startPrerelNum = rest.find_first_of("0123456789"); + if (startPrerelNum != 0) { // no prerelease num -> no match + return std::make_tuple(false, preReleaseType, preReleaseNum, false, + devNum); + } + + std::size_t endPrerelNum = rest.find_first_not_of("0123456789", 1); + // Works whether endPrerelNum is string::npos or not + string preReleaseNum_s = rest.substr(0, endPrerelNum); + preReleaseNum = strutils::readNonNegativeInt(preReleaseNum_s); + + if (preReleaseNum < 1) { + string msg = "invalid add-on version suffix: '" + suffix + "' " + "(prerelease number must be greater than or equal to 1, but got " + + preReleaseNum_s + ")"; + throw sg_format_exception(msg, suffix); + } + + rest = (endPrerelNum == string::npos) ? "" : rest.substr(endPrerelNum); + } + + if (strutils::starts_with(rest, ".dev")) { + rest = rest.substr(4); + std::size_t startDevNum = rest.find_first_of("0123456789"); + if (startDevNum != 0) { // no dev num -> no match + return std::make_tuple(false, preReleaseType, preReleaseNum, false, + devNum); + } + + std::size_t endDevNum = rest.find_first_not_of("0123456789", 1); + if (endDevNum != string::npos) { + // There is trailing garbage after the development release number + // -> no match + return std::make_tuple(false, preReleaseType, preReleaseNum, false, + devNum); + } + + devNum = strutils::readNonNegativeInt(rest); + if (devNum < 1) { + string msg = "invalid add-on version suffix: '" + suffix + "' " + "(development release number must be greater than or equal to 1, " + "but got " + rest + ")"; + throw sg_format_exception(msg, suffix); + } + + developmental = true; + } + + return std::make_tuple(true, preReleaseType, preReleaseNum, developmental, + devNum); +} + +// Static method +std::tuple +AddonVersionSuffix::popPrereleaseTypeFromBeginning(const string& s) +{ + if (s.empty()) { + return std::make_tuple(AddonVersionSuffixPrereleaseType::none, s); + } else if (s[0] == 'a') { + return std::make_tuple(AddonVersionSuffixPrereleaseType::alpha, + s.substr(1)); + } else if (s[0] == 'b') { + return std::make_tuple(AddonVersionSuffixPrereleaseType::beta, s.substr(1)); + } else if (strutils::starts_with(s, "rc")) { + return std::make_tuple(AddonVersionSuffixPrereleaseType::candidate, + s.substr(2)); + } + + return std::make_tuple(AddonVersionSuffixPrereleaseType::none, s); +} + +// Beware, this is not suitable for sorting! cf. genSortKey() below. +std::tuple +AddonVersionSuffix::makeTuple() const +{ + return std::make_tuple(_preReleaseType, _preReleaseNum, _developmental, + _devNum); +} + +std::tuple::type, + int, int, int> +AddonVersionSuffix::genSortKey() const +{ + using AddonRelType = AddonVersionSuffixPrereleaseType; + + // The first element means that a plain .devN is lower than everything else, + // except .devM with M <= N (namely: all dev and non-dev alpha, beta, + // candidates, as well as the empty suffix). + return std::make_tuple( + ((_developmental && _preReleaseType == AddonRelType::none) ? 0 : 1), + enumValue(_preReleaseType), + _preReleaseNum, + (_developmental ? 0 : 1), // e.g., 1.0.3a2.devN < 1.0.3a2 for all N + _devNum); +} + +bool operator==(const AddonVersionSuffix& lhs, const AddonVersionSuffix& rhs) +{ return lhs.genSortKey() == rhs.genSortKey(); } + +bool operator!=(const AddonVersionSuffix& lhs, const AddonVersionSuffix& rhs) +{ return !operator==(lhs, rhs); } + +bool operator< (const AddonVersionSuffix& lhs, const AddonVersionSuffix& rhs) +{ return lhs.genSortKey() < rhs.genSortKey(); } + +bool operator> (const AddonVersionSuffix& lhs, const AddonVersionSuffix& rhs) +{ return operator<(rhs, lhs); } + +bool operator<=(const AddonVersionSuffix& lhs, const AddonVersionSuffix& rhs) +{ return !operator>(lhs, rhs); } + +bool operator>=(const AddonVersionSuffix& lhs, const AddonVersionSuffix& rhs) +{ return !operator<(lhs, rhs); } + +std::ostream& operator<<(std::ostream& os, + const AddonVersionSuffix& addonVersionSuffix) +{ return os << addonVersionSuffix.str(); } + +// *************************************************************************** +// * AddonVersion * +// *************************************************************************** + +AddonVersion::AddonVersion(int major, int minor, int patchLevel, + AddonVersionSuffix suffix) + : _major(major), + _minor(minor), + _patchLevel(patchLevel), + _suffix(std::move(suffix)) + { } + +// Construct an AddonVersion instance from a tuple (major, minor, patchLevel, +// suffix). This would be nicer with std::apply(), but it requires C++17. +AddonVersion::AddonVersion( + const std::tuple& t) + : AddonVersion(std::get<0>(t), std::get<1>(t), std::get<2>(t), std::get<3>(t)) +{ } + +AddonVersion::AddonVersion(const std::string& versionStr) + : AddonVersion(versionStringToTuple(versionStr)) +{ } + +AddonVersion::AddonVersion(const char* versionStr) + : AddonVersion(string(versionStr)) +{ } + +// Static method +std::tuple +AddonVersion::versionStringToTuple(const std::string& versionStr) +{ +#ifdef HAVE_WORKING_STD_REGEX + // Use a simplified variant of the syntax described in PEP 440 + // (always 3 components in the + // release segment, pre-release segment + development release segment; no + // post-release segment allowed). + std::regex versionRegexp(R"((\d+)\.(\d+).(\d+)(.*))"); + std::smatch results; + + if (std::regex_match(versionStr, results, versionRegexp)) { + const string majorNumber_s = results.str(1); + const string minorNumber_s = results.str(2); + const string patchLevel_s = results.str(3); + const string suffix_s = results.str(4); + + int major = strutils::readNonNegativeInt(majorNumber_s); + int minor = strutils::readNonNegativeInt(minorNumber_s); + int patchLevel = strutils::readNonNegativeInt(patchLevel_s); + + return std::make_tuple(major, minor, patchLevel, + AddonVersionSuffix(suffix_s)); +#else // all this 'else' clause should be removed once we actually require C++11 + bool isMatch; + int major, minor, patchLevel; + AddonVersionSuffix suffix; + + std::tie(isMatch, major, minor, patchLevel, suffix) = + parseVersionString_noRegexp(versionStr); + + if (isMatch) { + return std::make_tuple(major, minor, patchLevel, suffix); +#endif // HAVE_WORKING_STD_REGEX + } else { // the regexp didn't match + string msg = "invalid add-on version number: '" + versionStr + "' " + "(expected form is MAJOR.MINOR.PATCHLEVEL[{a|b|rc}N1][.devN2] where " + "N1 and N2 are positive integers)"; + throw sg_format_exception(msg, versionStr); + } +} + +// Static method, only needed for compilers that are not C++11-compliant +// (gcc 4.8 pretends to support as required by C++11 but doesn't, see +// ). +std::tuple +AddonVersion::parseVersionString_noRegexp(const string& versionStr) +{ + int major = 0, minor = 0, patchLevel = 0; + AddonVersionSuffix suffix{}; + + // Major version number + std::size_t endMajor = versionStr.find_first_not_of("0123456789"); + if (endMajor == 0 || endMajor == string::npos) { // no match + return std::make_tuple(false, major, minor, patchLevel, suffix); + } + major = strutils::readNonNegativeInt(versionStr.substr(0, endMajor)); + + // Dot separating the major and minor version numbers + if (versionStr.size() < endMajor + 1 || versionStr[endMajor] != '.') { + return std::make_tuple(false, major, minor, patchLevel, suffix); + } + string rest = versionStr.substr(endMajor + 1); + + // Minor version number + std::size_t endMinor = rest.find_first_not_of("0123456789"); + if (endMinor == 0 || endMinor == string::npos) { // no match + return std::make_tuple(false, major, minor, patchLevel, suffix); + } + minor = strutils::readNonNegativeInt(rest.substr(0, endMinor)); + + // Dot separating the minor version number and the patch level + if (rest.size() < endMinor + 1 || rest[endMinor] != '.') { + return std::make_tuple(false, major, minor, patchLevel, suffix); + } + rest = rest.substr(endMinor + 1); + + // Patch level + std::size_t endPatchLevel = rest.find_first_not_of("0123456789"); + if (endPatchLevel == 0) { // no patch level, therefore no match + return std::make_tuple(false, major, minor, patchLevel, suffix); + } + patchLevel = strutils::readNonNegativeInt(rest.substr(0, endPatchLevel)); + + if (endPatchLevel != string::npos) { // there is a version suffix, parse it + suffix = AddonVersionSuffix(rest.substr(endPatchLevel)); + } + + return std::make_tuple(true, major, minor, patchLevel, suffix); +} + +int AddonVersion::majorNumber() const +{ return _major; } + +int AddonVersion::minorNumber() const +{ return _minor; } + +int AddonVersion::patchLevel() const +{ return _patchLevel; } + +AddonVersionSuffix AddonVersion::suffix() const +{ return _suffix; } + +std::string AddonVersion::suffixStr() const +{ return suffix().str(); } + +std::tuple AddonVersion::makeTuple() const +{ + return std::make_tuple(majorNumber(), minorNumber(), patchLevel(), suffix()); +} + +string AddonVersion::str() const +{ + // Assemble the major.minor.patchLevel string + vector v({majorNumber(), minorNumber(), patchLevel()}); + string relSeg = std::accumulate(std::next(v.begin()), v.end(), + std::to_string(v[0]), + [](string s, int num) { + return s + '.' + std::to_string(num); + }); + + // Concatenate with the suffix string + return relSeg + suffixStr(); +} + + +bool operator==(const AddonVersion& lhs, const AddonVersion& rhs) +{ return lhs.makeTuple() == rhs.makeTuple(); } + +bool operator!=(const AddonVersion& lhs, const AddonVersion& rhs) +{ return !operator==(lhs, rhs); } + +bool operator< (const AddonVersion& lhs, const AddonVersion& rhs) +{ return lhs.makeTuple() < rhs.makeTuple(); } + +bool operator> (const AddonVersion& lhs, const AddonVersion& rhs) +{ return operator<(rhs, lhs); } + +bool operator<=(const AddonVersion& lhs, const AddonVersion& rhs) +{ return !operator>(lhs, rhs); } + +bool operator>=(const AddonVersion& lhs, const AddonVersion& rhs) +{ return !operator<(lhs, rhs); } + +std::ostream& operator<<(std::ostream& os, const AddonVersion& addonVersion) +{ return os << addonVersion.str(); } + + +// *************************************************************************** +// * For the Nasal bindings * +// *************************************************************************** + +bool AddonVersion::equal(const nasal::CallContext& ctx) const +{ + auto other = ctx.requireArg(0); + return *this == *other; +} + +bool AddonVersion::nonEqual(const nasal::CallContext& ctx) const +{ + auto other = ctx.requireArg(0); + return *this != *other; +} + +bool AddonVersion::lowerThan(const nasal::CallContext& ctx) const +{ + auto other = ctx.requireArg(0); + return *this < *other; +} + +bool AddonVersion::lowerThanOrEqual(const nasal::CallContext& ctx) const +{ + auto other = ctx.requireArg(0); + return *this <= *other; +} + +bool AddonVersion::greaterThan(const nasal::CallContext& ctx) const +{ + auto other = ctx.requireArg(0); + return *this > *other; +} + +bool AddonVersion::greaterThanOrEqual(const nasal::CallContext& ctx) const +{ + auto other = ctx.requireArg(0); + return *this >= *other; +} + +// Static method +void AddonVersion::setupGhost(nasal::Hash& addonsModule) +{ + nasal::Ghost::init("addons.AddonVersion") + .member("majorNumber", &AddonVersion::majorNumber) + .member("minorNumber", &AddonVersion::minorNumber) + .member("patchLevel", &AddonVersion::patchLevel) + .member("suffix", &AddonVersion::suffixStr) + .method("str", &AddonVersion::str) + .method("equal", &AddonVersion::equal) + .method("nonEqual", &AddonVersion::nonEqual) + .method("lowerThan", &AddonVersion::lowerThan) + .method("lowerThanOrEqual", &AddonVersion::lowerThanOrEqual) + .method("greaterThan", &AddonVersion::greaterThan) + .method("greaterThanOrEqual", &AddonVersion::greaterThanOrEqual); +} + +} // of namespace addons + +} // of namespace flightgear diff -Nru flightgear-2017.3.1+dfsg/src/Add-ons/AddonVersion.hxx flightgear-2018.1.1+dfsg/src/Add-ons/AddonVersion.hxx --- flightgear-2017.3.1+dfsg/src/Add-ons/AddonVersion.hxx 1970-01-01 00:00:00.000000000 +0000 +++ flightgear-2018.1.1+dfsg/src/Add-ons/AddonVersion.hxx 2018-04-06 19:43:33.000000000 +0000 @@ -0,0 +1,212 @@ +// -*- coding: utf-8 -*- +// +// AddonVersion.hxx --- Version class for FlightGear add-ons +// Copyright (C) 2017 Florent Rougon +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +#ifndef FG_ADDONVERSION_HXX +#define FG_ADDONVERSION_HXX + +#include +#include +#include +#include + +#include +#include +#include + +#include "addon_fwd.hxx" + +namespace flightgear +{ + +namespace addons +{ + +// Order matters for the sorting/comparison functions +enum class AddonVersionSuffixPrereleaseType { + alpha = 0, + beta, + candidate, + none +}; + +// *************************************************************************** +// * AddonVersionSuffix * +// *************************************************************************** + +class AddonVersionSuffix +{ +public: + AddonVersionSuffix(AddonVersionSuffixPrereleaseType _preReleaseType + = AddonVersionSuffixPrereleaseType::none, + int preReleaseNum = 0, bool developmental = false, + int devNum = 0); + // Construct from a string. The empty string is a valid input. + AddonVersionSuffix(const std::string& suffix); + AddonVersionSuffix(const char* suffix); + // Construct from a tuple + explicit AddonVersionSuffix( + const std::tuple& t); + + // Return all components of an AddonVersionSuffix instance as a tuple. + // Beware, this is not suitable for sorting! cf. genSortKey() below. + std::tuple makeTuple() const; + + // String representation of an AddonVersionSuffix + std::string str() const; + +private: + // String representation of the release type component: "a", "b", "rc" or "". + static std::string releaseTypeStr(AddonVersionSuffixPrereleaseType); + + // If 's' starts with a non-empty release type ('a', 'b' or 'rc'), return + // the corresponding enum value along with the remainder of 's' (that is, + // everything after the release type). Otherwise, return + // AddonVersionSuffixPrereleaseType::none along with a copy of 's'. + static std::tuple + popPrereleaseTypeFromBeginning(const std::string& s); + + // Extract all components from a string representing a version suffix. + // The components of the return value are, in this order: + // + // preReleaseType, preReleaseNum, developmental, devNum + // + // Note: the empty string is a valid input. + static std::tuple + suffixStringToTuple(const std::string& suffix); + + // Used to implement suffixStringToTuple() for compilers that are not + // C++11-compliant (gcc 4.8 pretends to support as required by C++11 + // but doesn't, see ). + // + // The bool in the first component of the result is true iff 'suffix' is a + // valid version suffix string. The bool is false when 'suffix' is invalid + // in such a way that the generic sg_format_exception thrown at the end of + // suffixStringToTuple() is appropriate. In all other cases, a specific + // exception is thrown. + static std::tuple + parseVersionSuffixString_noRegexp(const std::string& suffix); + + // Useful for comparisons/sorting purposes + std::tuple::type, + int, int, int> genSortKey() const; + + friend bool operator==(const AddonVersionSuffix& lhs, + const AddonVersionSuffix& rhs); + friend bool operator<(const AddonVersionSuffix& lhs, + const AddonVersionSuffix& rhs); + + AddonVersionSuffixPrereleaseType _preReleaseType; + int _preReleaseNum; // integer >= 1 (0 when not applicable) + bool _developmental; // whether the suffix ends with '.devN' + int _devNum; // integer >= 1 (0 when not applicable) +}; + + +// operator==() and operator<() are declared above. +bool operator!=(const AddonVersionSuffix& lhs, const AddonVersionSuffix& rhs); +bool operator> (const AddonVersionSuffix& lhs, const AddonVersionSuffix& rhs); +bool operator<=(const AddonVersionSuffix& lhs, const AddonVersionSuffix& rhs); +bool operator>=(const AddonVersionSuffix& lhs, const AddonVersionSuffix& rhs); + +std::ostream& operator<<(std::ostream&, const AddonVersionSuffix&); + +// *************************************************************************** +// * AddonVersion * +// *************************************************************************** + +// I suggest to use either the year-based FlightGear-type versioning, or +// semantic versioning (). For the suffix, we allow things +// like "a1" (alpha1), "b2" (beta2), "rc4" (release candidate 4), "a1.dev3" +// (development release for "a1", which sorts before "a1"), etc. It's a subset +// of the syntax allowed in . +class AddonVersion : public SGReferenced +{ +public: + AddonVersion(int major = 0, int minor = 0, int patchLevel = 0, + AddonVersionSuffix suffix = AddonVersionSuffix()); + AddonVersion(const std::string& version); + AddonVersion(const char* version); + explicit AddonVersion(const std::tuple& t); + + // Using the method names major() and minor() can lead to incomprehensible + // errors such as "major is not a member of flightgear::addons::AddonVersion" + // because of a hideous glibc bug[1]: major() and minor() are defined by + // standard headers as *macros*! + // + // [1] https://bugzilla.redhat.com/show_bug.cgi?id=130601 + int majorNumber() const; + int minorNumber() const; + int patchLevel() const; + AddonVersionSuffix suffix() const; + std::string suffixStr() const; + + std::string str() const; + + // For the Nasal bindings (otherwise, we have operator==(), etc.) + bool equal(const nasal::CallContext& ctx) const; + bool nonEqual(const nasal::CallContext& ctx) const; + bool lowerThan(const nasal::CallContext& ctx) const; + bool lowerThanOrEqual(const nasal::CallContext& ctx) const; + bool greaterThan(const nasal::CallContext& ctx) const; + bool greaterThanOrEqual(const nasal::CallContext& ctx) const; + + static void setupGhost(nasal::Hash& addonsModule); + +private: + // Useful for comparisons/sorting purposes + std::tuple makeTuple() const; + + static std::tuple + versionStringToTuple(const std::string& versionStr); + + // Used to implement versionStringToTuple() for compilers that are not + // C++11-compliant (gcc 4.8 pretends to support as required by C++11 + // but doesn't, see ). + // + // The bool in the first component of the result is true iff 'versionStr' is + // a valid version string. The bool is false when 'versionStr' is invalid in + // such a way that the generic sg_format_exception thrown at the end of + // versionStringToTuple() is appropriate. In all other cases, a specific + // exception is thrown. + static std::tuple + parseVersionString_noRegexp(const std::string& versionStr); + + friend bool operator==(const AddonVersion& lhs, const AddonVersion& rhs); + friend bool operator<(const AddonVersion& lhs, const AddonVersion& rhs); + + int _major; + int _minor; + int _patchLevel; + AddonVersionSuffix _suffix; +}; + +// operator==() and operator<() are declared above. +bool operator!=(const AddonVersion& lhs, const AddonVersion& rhs); +bool operator> (const AddonVersion& lhs, const AddonVersion& rhs); +bool operator<=(const AddonVersion& lhs, const AddonVersion& rhs); +bool operator>=(const AddonVersion& lhs, const AddonVersion& rhs); + +std::ostream& operator<<(std::ostream&, const AddonVersion&); + +} // of namespace addons + +} // of namespace flightgear + +#endif // of FG_ADDONVERSION_HXX diff -Nru flightgear-2017.3.1+dfsg/src/Add-ons/CMakeLists.txt flightgear-2018.1.1+dfsg/src/Add-ons/CMakeLists.txt --- flightgear-2017.3.1+dfsg/src/Add-ons/CMakeLists.txt 1970-01-01 00:00:00.000000000 +0000 +++ flightgear-2018.1.1+dfsg/src/Add-ons/CMakeLists.txt 2018-04-06 19:43:33.000000000 +0000 @@ -0,0 +1,27 @@ +include(FlightGearComponent) + +set(SOURCES Addon.cxx + AddonManager.cxx + AddonMetadataParser.cxx + AddonResourceProvider.cxx + AddonVersion.cxx + contacts.cxx + exceptions.cxx + ) + +set(HEADERS addon_fwd.hxx + Addon.hxx + AddonManager.hxx + AddonMetadataParser.hxx + AddonResourceProvider.hxx + AddonVersion.hxx + contacts.hxx + exceptions.hxx + pointer_traits.hxx + ) + +flightgear_component(AddonManagement "${SOURCES}" "${HEADERS}") + +if (COMMAND flightgear_test) + flightgear_test(test_AddonManagement test_AddonManagement.cxx) +endif() diff -Nru flightgear-2017.3.1+dfsg/src/Add-ons/contacts.cxx flightgear-2018.1.1+dfsg/src/Add-ons/contacts.cxx --- flightgear-2017.3.1+dfsg/src/Add-ons/contacts.cxx 1970-01-01 00:00:00.000000000 +0000 +++ flightgear-2018.1.1+dfsg/src/Add-ons/contacts.cxx 2018-04-06 19:43:33.000000000 +0000 @@ -0,0 +1,127 @@ +// -*- coding: utf-8 -*- +// +// contacts.cxx --- FlightGear classes holding add-on contact metadata +// Copyright (C) 2018 Florent Rougon +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +#include +#include + +#include +#include +#include +#include + +#include "addon_fwd.hxx" +#include "contacts.hxx" + +using std::string; +using simgear::enumValue; + +namespace flightgear +{ + +namespace addons +{ + +// *************************************************************************** +// * Contact * +// *************************************************************************** + +Contact::Contact(ContactType type, string name, string email, string url) + : _type(type), + _name(std::move(name)), + _email(std::move(email)), + _url(std::move(url)) +{ } + +ContactType Contact::getType() const +{ return _type; } + +string Contact::getTypeString() const +{ + switch (getType()) { + case ContactType::author: + return "author"; + case ContactType::maintainer: + return "maintainer"; + default: + throw sg_error("unexpected value for member of " + "flightgear::addons::ContactType: " + + std::to_string(enumValue(getType()))); + } +} + +string Contact::getName() const +{ return _name; } + +void Contact::setName(const string& name) +{ _name = name; } + +string Contact::getEmail() const +{ return _email; } + +void Contact::setEmail(const string& email) +{ _email = email; } + +string Contact::getUrl() const +{ return _url; } + +void Contact::setUrl(const string& url) +{ _url = url; } + +// Static method +void Contact::setupGhost(nasal::Hash& addonsModule) +{ + nasal::Ghost::init("addons.Contact") + .member("name", &Contact::getName) + .member("email", &Contact::getEmail) + .member("url", &Contact::getUrl); +} + +// *************************************************************************** +// * Author * +// *************************************************************************** + +Author::Author(string name, string email, string url) + : Contact(ContactType::author, name, email, url) +{ } + +// Static method +void Author::setupGhost(nasal::Hash& addonsModule) +{ + nasal::Ghost::init("addons.Author") + .bases(); +} + +// *************************************************************************** +// * Maintainer * +// *************************************************************************** + +Maintainer::Maintainer(string name, string email, string url) + : Contact(ContactType::maintainer, name, email, url) +{ } + +// Static method +void Maintainer::setupGhost(nasal::Hash& addonsModule) +{ + nasal::Ghost::init("addons.Maintainer") + .bases(); +} + +} // of namespace addons + +} // of namespace flightgear diff -Nru flightgear-2017.3.1+dfsg/src/Add-ons/contacts.hxx flightgear-2018.1.1+dfsg/src/Add-ons/contacts.hxx --- flightgear-2017.3.1+dfsg/src/Add-ons/contacts.hxx 1970-01-01 00:00:00.000000000 +0000 +++ flightgear-2018.1.1+dfsg/src/Add-ons/contacts.hxx 2018-04-06 19:43:33.000000000 +0000 @@ -0,0 +1,126 @@ +// -*- coding: utf-8 -*- +// +// contacts.hxx --- FlightGear classes holding add-on contact metadata +// Copyright (C) 2018 Florent Rougon +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +#ifndef FG_ADDON_CONTACTS_HXX +#define FG_ADDON_CONTACTS_HXX + +#include + +#include + +#include "addon_fwd.hxx" + +namespace nasal +{ + class Hash; // forward declaration +}; + +namespace flightgear +{ + +namespace addons +{ + +enum class ContactType { + author, + maintainer +}; + +// Class used to store info about an author or maintainer (possibly also a +// mailing-list, things like that) +class Contact : public SGReferenced +{ +public: + Contact(ContactType type, std::string name, std::string email = "", + std::string url = ""); + virtual ~Contact() = default; + + ContactType getType() const; + std::string getTypeString() const; + + std::string getName() const; + void setName(const std::string& name); + + std::string getEmail() const; + void setEmail(const std::string& email); + + std::string getUrl() const; + void setUrl(const std::string& url); + + static void setupGhost(nasal::Hash& addonsModule); + +private: + const ContactType _type; + std::string _name; + std::string _email; + std::string _url; +}; + +class Author : public Contact +{ +public: + Author(std::string name, std::string email = "", std::string url = ""); + + static void setupGhost(nasal::Hash& addonsModule); +}; + +class Maintainer : public Contact +{ +public: + Maintainer(std::string name, std::string email = "", std::string url = ""); + + static void setupGhost(nasal::Hash& addonsModule); +}; + +// *************************************************************************** +// * contact_traits * +// *************************************************************************** + +template +struct contact_traits; + +template<> +struct contact_traits +{ + using contact_type = Author; + using strong_ref = AuthorRef; + + static std::string xmlNodeName() + { + return "author"; + } +}; + +template<> +struct contact_traits +{ + using contact_type = Maintainer; + using strong_ref = MaintainerRef; + + static std::string xmlNodeName() + { + return "maintainer"; + } +}; + +} // of namespace addons + +} // of namespace flightgear + +#endif // of FG_ADDON_CONTACTS_HXX diff -Nru flightgear-2017.3.1+dfsg/src/Add-ons/exceptions.cxx flightgear-2018.1.1+dfsg/src/Add-ons/exceptions.cxx --- flightgear-2017.3.1+dfsg/src/Add-ons/exceptions.cxx 1970-01-01 00:00:00.000000000 +0000 +++ flightgear-2018.1.1+dfsg/src/Add-ons/exceptions.cxx 2018-04-06 19:43:33.000000000 +0000 @@ -0,0 +1,55 @@ +// -*- coding: utf-8 -*- +// +// exceptions.cxx --- Exception classes for the FlightGear add-on infrastructure +// Copyright (C) 2017 Florent Rougon +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +#include + +#include + +#include "exceptions.hxx" + +using std::string; + +namespace flightgear +{ + +namespace addons +{ + +namespace errors +{ + +// *************************************************************************** +// * Base class for add-on exceptions * +// *************************************************************************** + +// Prepending a prefix such as "Add-on error: " would be redundant given the +// messages used in, e.g., the Addon class code. +error::error(const string& message, const string& origin) + : sg_exception(message, origin) +{ } + +error::error(const char* message, const char* origin) + : error(string(message), string(origin)) +{ } + +} // of namespace errors + +} // of namespace addons + +} // of namespace flightgear diff -Nru flightgear-2017.3.1+dfsg/src/Add-ons/exceptions.hxx flightgear-2018.1.1+dfsg/src/Add-ons/exceptions.hxx --- flightgear-2017.3.1+dfsg/src/Add-ons/exceptions.hxx 1970-01-01 00:00:00.000000000 +0000 +++ flightgear-2018.1.1+dfsg/src/Add-ons/exceptions.hxx 2018-04-06 19:43:33.000000000 +0000 @@ -0,0 +1,71 @@ +// -*- coding: utf-8 -*- +// +// exceptions.hxx --- Exception classes for the FlightGear add-on infrastructure +// Copyright (C) 2017 Florent Rougon +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +#ifndef FG_ADDON_EXCEPTIONS_HXX +#define FG_ADDON_EXCEPTIONS_HXX + +#include + +#include + +namespace flightgear +{ + +namespace addons +{ + +namespace errors +{ + +class error : public sg_exception +{ +public: + explicit error(const std::string& message, + const std::string& origin = std::string()); + explicit error(const char* message, const char* origin = nullptr); +}; + +class error_loading_config_file : public error +{ using error::error; /* inherit all constructors */ }; + +class no_metadata_file_found : public error +{ using error::error; }; + +class error_loading_metadata_file : public error +{ using error::error; }; + +class duplicate_registration_attempt : public error +{ using error::error; }; + +class fg_version_too_old : public error +{ using error::error; }; + +class fg_version_too_recent : public error +{ using error::error; }; + +class invalid_resource_path : public error +{ using error::error; }; + +} // of namespace errors + +} // of namespace addons + +} // of namespace flightgear + +#endif // of FG_ADDON_EXCEPTIONS_HXX diff -Nru flightgear-2017.3.1+dfsg/src/Add-ons/pointer_traits.hxx flightgear-2018.1.1+dfsg/src/Add-ons/pointer_traits.hxx --- flightgear-2017.3.1+dfsg/src/Add-ons/pointer_traits.hxx 1970-01-01 00:00:00.000000000 +0000 +++ flightgear-2018.1.1+dfsg/src/Add-ons/pointer_traits.hxx 2018-04-06 19:43:33.000000000 +0000 @@ -0,0 +1,67 @@ +// -*- coding: utf-8 -*- +// +// pointer_traits.hxx --- Pointer traits classes +// Copyright (C) 2018 Florent Rougon +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +#ifndef FG_ADDON_POINTER_TRAITS_HXX +#define FG_ADDON_POINTER_TRAITS_HXX + +#include +#include + +#include + +namespace flightgear +{ + +namespace addons +{ + +template +struct shared_ptr_traits; + +template +struct shared_ptr_traits> +{ + using element_type = T; + using strong_ref = SGSharedPtr; + + template + static strong_ref makeStrongRef(Args&& ...args) + { + return strong_ref(new T(std::forward(args)...)); + } +}; + +template +struct shared_ptr_traits> +{ + using element_type = T; + using strong_ref = std::shared_ptr; + + template + static strong_ref makeStrongRef(Args&& ...args) + { + return std::make_shared(std::forward(args)...); + } +}; + +} // of namespace addons + +} // of namespace flightgear + +#endif // of FG_ADDON_POINTER_TRAITS_HXX diff -Nru flightgear-2017.3.1+dfsg/src/Add-ons/test_AddonManagement.cxx flightgear-2018.1.1+dfsg/src/Add-ons/test_AddonManagement.cxx --- flightgear-2017.3.1+dfsg/src/Add-ons/test_AddonManagement.cxx 1970-01-01 00:00:00.000000000 +0000 +++ flightgear-2018.1.1+dfsg/src/Add-ons/test_AddonManagement.cxx 2018-04-06 19:43:33.000000000 +0000 @@ -0,0 +1,255 @@ +// -*- coding: utf-8 -*- +// +// test_AddonManagement.cxx --- Automated tests for FlightGear classes dealing +// with add-ons +// Copyright (C) 2017 Florent Rougon +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +#include "config.h" +#include "unitTestHelpers.hxx" + +#include +#include +#include +#include +#include + +#include +#include + +#include + +#include "Add-ons/Addon.hxx" +#include "Add-ons/AddonVersion.hxx" + +using std::string; +using std::vector; + +using flightgear::addons::Addon; +using flightgear::addons::AddonVersion; +using flightgear::addons::AddonVersionSuffix; + + +void testAddonVersionSuffix() +{ + using AddonRelType = flightgear::addons::AddonVersionSuffixPrereleaseType; + + fgtest::initTestGlobals("AddonVersionSuffix"); + + AddonVersionSuffix v1(AddonRelType::beta, 2, true, 5); + AddonVersionSuffix v1Copy(v1); + AddonVersionSuffix v1NonDev(AddonRelType::beta, 2, false); + SG_CHECK_EQUAL(v1, v1Copy); + SG_CHECK_EQUAL(v1, AddonVersionSuffix("b2.dev5")); + SG_CHECK_EQUAL_NOSTREAM(v1.makeTuple(), + std::make_tuple(AddonRelType::beta, 2, true, 5)); + SG_CHECK_EQUAL(AddonVersionSuffix(), AddonVersionSuffix("")); + // A simple comparison + SG_CHECK_LT(v1, v1NonDev); // b2.dev5 < b2 + + // Check string representation with str() + SG_CHECK_EQUAL(AddonVersionSuffix(AddonRelType::none).str(), ""); + SG_CHECK_EQUAL(AddonVersionSuffix(AddonRelType::none, 0, true, 12).str(), + ".dev12"); + SG_CHECK_EQUAL(AddonVersionSuffix(AddonRelType::alpha, 1).str(), "a1"); + SG_CHECK_EQUAL(AddonVersionSuffix(AddonRelType::alpha, 1, false).str(), "a1"); + SG_CHECK_EQUAL(AddonVersionSuffix(AddonRelType::alpha, 2, true, 4).str(), + "a2.dev4"); + + SG_CHECK_EQUAL(AddonVersionSuffix(AddonRelType::beta, 1).str(), "b1"); + SG_CHECK_EQUAL(AddonVersionSuffix(AddonRelType::beta, 1, false).str(), "b1"); + SG_CHECK_EQUAL(AddonVersionSuffix(AddonRelType::beta, 2, true, 4).str(), + "b2.dev4"); + + SG_CHECK_EQUAL(AddonVersionSuffix(AddonRelType::candidate, 1).str(), "rc1"); + SG_CHECK_EQUAL(AddonVersionSuffix(AddonRelType::candidate, 1, false).str(), + "rc1"); + SG_CHECK_EQUAL(AddonVersionSuffix(AddonRelType::candidate, 2, true, 4).str(), + "rc2.dev4"); + + // Check stream output + std::ostringstream oss; + oss << AddonVersionSuffix(AddonRelType::candidate, 2, true, 4); + SG_CHECK_EQUAL(oss.str(), "rc2.dev4"); + + // Check ordering with all types of transitions, using operator<() + auto checkStrictOrdering = [](const vector& v) { + assert(v.size() > 1); + for (std::size_t i=0; i < v.size() - 1; i++) { + SG_CHECK_LT(v[i], v[i+1]); + } + }; + + checkStrictOrdering({ + {AddonRelType::none, 0, true, 1}, + {AddonRelType::none, 0, true, 2}, + {AddonRelType::alpha, 1, true, 1}, + {AddonRelType::alpha, 1, true, 2}, + {AddonRelType::alpha, 1, true, 3}, + {AddonRelType::alpha, 1, false}, + {AddonRelType::alpha, 2, true, 1}, + {AddonRelType::alpha, 2, true, 3}, + {AddonRelType::alpha, 2, false}, + {AddonRelType::beta, 1, true, 1}, + {AddonRelType::beta, 1, true, 25}, + {AddonRelType::beta, 1, false}, + {AddonRelType::beta, 2, true, 1}, + {AddonRelType::beta, 2, true, 2}, + {AddonRelType::beta, 2, false}, + {AddonRelType::candidate, 1, true, 1}, + {AddonRelType::candidate, 1, true, 2}, + {AddonRelType::candidate, 1, false}, + {AddonRelType::candidate, 2, true, 1}, + {AddonRelType::candidate, 2, true, 2}, + {AddonRelType::candidate, 2, false}, + {AddonRelType::candidate, 21, false}, + {AddonRelType::none} + }); + + // Check operator>() and operator!=() + SG_CHECK_GT(AddonVersionSuffix(AddonRelType::none), + AddonVersionSuffix(AddonRelType::candidate, 21, false)); + + SG_CHECK_NE(AddonVersionSuffix(AddonRelType::none), + AddonVersionSuffix(AddonRelType::candidate, 21, false)); + + // Check operator<=() and operator>=() + SG_CHECK_LE(AddonVersionSuffix(AddonRelType::candidate, 2, false), + AddonVersionSuffix(AddonRelType::candidate, 2, false)); + SG_CHECK_LE(AddonVersionSuffix(AddonRelType::candidate, 2, false), + AddonVersionSuffix(AddonRelType::none)); + + SG_CHECK_GE(AddonVersionSuffix(AddonRelType::none), + AddonVersionSuffix(AddonRelType::none)); + SG_CHECK_GE(AddonVersionSuffix(AddonRelType::none), + AddonVersionSuffix(AddonRelType::candidate, 21, false)); + + fgtest::shutdownTestGlobals(); +} + +void testAddonVersion() +{ + using AddonRelType = flightgear::addons::AddonVersionSuffixPrereleaseType; + + fgtest::initTestGlobals("AddonVersion"); + + AddonVersionSuffix suffix(AddonRelType::beta, 2, true, 5); + AddonVersion v1(2017, 4, 7, suffix); + AddonVersion v1Copy(v1); + AddonVersion v2 = v1; + AddonVersion v3(std::move(v1Copy)); + AddonVersion v4 = std::move(v2); + + SG_CHECK_EQUAL(v1, AddonVersion("2017.4.7b2.dev5")); + SG_CHECK_EQUAL(v1, AddonVersion(std::make_tuple(2017, 4, 7, suffix))); + SG_CHECK_EQUAL(v1, v3); + SG_CHECK_EQUAL(v1, v4); + SG_CHECK_LT(v1, AddonVersion("2017.4.7b2")); + SG_CHECK_LE(v1, AddonVersion("2017.4.7b2")); + SG_CHECK_LE(v1, v1); + SG_CHECK_GT(AddonVersion("2017.4.7b2"), v1); + SG_CHECK_GE(AddonVersion("2017.4.7b2"), v1); + SG_CHECK_GE(v1, v1); + SG_CHECK_NE(v1, AddonVersion("2017.4.7b3")); + + SG_CHECK_EQUAL(v1.majorNumber(), 2017); + SG_CHECK_EQUAL(v1.minorNumber(), 4); + SG_CHECK_EQUAL(v1.patchLevel(), 7); + SG_CHECK_EQUAL(v1.suffix(), suffix); + + // Round-trips std::string <-> AddonVersion + SG_CHECK_EQUAL(AddonVersion("2017.4.7.dev13").str(), "2017.4.7.dev13"); + SG_CHECK_EQUAL(AddonVersion("2017.4.7a2.dev8").str(), "2017.4.7a2.dev8"); + SG_CHECK_EQUAL(AddonVersion("2017.4.7a2").str(), "2017.4.7a2"); + SG_CHECK_EQUAL(AddonVersion("2017.4.7b2.dev5").str(), "2017.4.7b2.dev5"); + SG_CHECK_EQUAL(AddonVersion("2017.4.7b2").str(), "2017.4.7b2"); + SG_CHECK_EQUAL(AddonVersion("2017.4.7rc1.dev3").str(), "2017.4.7rc1.dev3"); + SG_CHECK_EQUAL(AddonVersion("2017.4.7rc1").str(), "2017.4.7rc1"); + SG_CHECK_EQUAL(AddonVersion("2017.4.7").str(), "2017.4.7"); + + // Check stream output + std::ostringstream oss; + oss << AddonVersion("2017.4.7b2.dev5"); + SG_CHECK_EQUAL(oss.str(), "2017.4.7b2.dev5"); + + // Check ordering with all types of transitions, using operator<() + auto checkStrictOrdering = [](const vector& v) { + assert(v.size() > 1); + for (std::size_t i=0; i < v.size() - 1; i++) { + SG_CHECK_LT(v[i], v[i+1]); + } + }; + + checkStrictOrdering({ + "3.12.8.dev1", "3.12.8.dev2", "3.12.8.dev12", "3.12.8a1.dev1", + "3.12.8a1.dev2", "3.12.8a1", "3.12.8a2", "3.12.8b1.dev1", + "3.12.8b1.dev2", "3.12.8b1", "3.12.8b2", "3.12.8b10", + "3.12.8rc1.dev1", "3.12.8rc1.dev2", "3.12.8rc1.dev3", + "3.12.8rc1", "3.12.8rc2", "3.12.8rc3", "3.12.8", "3.12.9.dev1", + "3.12.9", "3.13.0", "4.0.0.dev1", "4.0.0.dev10", "4.0.0a1", "4.0.0", + "2017.4.0", "2017.4.1", "2017.4.10", "2017.5.0", "2018.0.0"}); + + fgtest::shutdownTestGlobals(); +} + +void testAddon() +{ + fgtest::initTestGlobals("Addon"); + + Addon addon; + std::string addonId = "org.FlightGear.addons.MyGreatAddon"; + addon.setId(addonId); + addon.setVersion(AddonVersion("2017.2.5rc3")); + addon.setBasePath(SGPath("/path/to/MyGreatAddon")); + addon.setMinFGVersionRequired("2017.4.1"); + addon.setMaxFGVersionRequired("none"); + + SG_CHECK_EQUAL(addon.getId(), addonId); + SG_CHECK_EQUAL(*addon.getVersion(), AddonVersion("2017.2.5rc3")); + SG_CHECK_EQUAL(addon.getBasePath(), SGPath("/path/to/MyGreatAddon")); + SG_CHECK_EQUAL(addon.getMinFGVersionRequired(), "2017.4.1"); + + const string refText = "addon '" + addonId + "' (version = 2017.2.5rc3, " + "base path = '/path/to/MyGreatAddon', " + "minFGVersionRequired = '2017.4.1', " + "maxFGVersionRequired = 'none')"; + SG_CHECK_EQUAL(addon.str(), refText); + + // Check stream output + std::ostringstream oss; + oss << addon; + SG_CHECK_EQUAL(oss.str(), refText); + + // Set a max FG version and recheck + addon.setMaxFGVersionRequired("2018.2.5"); + const string refText2 = "addon '" + addonId + "' (version = 2017.2.5rc3, " + "base path = '/path/to/MyGreatAddon', " + "minFGVersionRequired = '2017.4.1', " + "maxFGVersionRequired = '2018.2.5')"; + SG_CHECK_EQUAL(addon.getMaxFGVersionRequired(), "2018.2.5"); + SG_CHECK_EQUAL(addon.str(), refText2); + + fgtest::shutdownTestGlobals(); +} + +int main(int argc, const char* const* argv) +{ + testAddonVersionSuffix(); + testAddonVersion(); + testAddon(); + + return EXIT_SUCCESS; +} diff -Nru flightgear-2017.3.1+dfsg/src/AIModel/AIAircraft.cxx flightgear-2018.1.1+dfsg/src/AIModel/AIAircraft.cxx --- flightgear-2017.3.1+dfsg/src/AIModel/AIAircraft.cxx 2017-09-20 07:51:30.000000000 +0000 +++ flightgear-2018.1.1+dfsg/src/AIModel/AIAircraft.cxx 2018-04-06 19:43:33.000000000 +0000 @@ -247,24 +247,15 @@ return; } - FGAIFlightPlan* fp = new FGAIFlightPlan(flightplan); - if (fp->isValidPlan()) { - fp->setRepeat(repeat); - SetFlightPlan(fp); + std::unique_ptr plan(new FGAIFlightPlan(flightplan)); + if (plan->isValidPlan()) { + plan->setRepeat(repeat); + FGAIBase::setFlightPlan(std::move(plan)); } else { SG_LOG(SG_AI, SG_WARN, "setFlightPlan: invalid flightplan specified:" << flightplan); - delete fp; } } - -void FGAIAircraft::SetFlightPlan(FGAIFlightPlan *f) -{ - delete fp; - fp = f; -} - - void FGAIAircraft::ProcessFlightPlan( double dt, time_t now ) { // the one behind you @@ -634,7 +625,7 @@ } prevController = controller; if (controller) { - controller->announcePosition(getID(), fp, fp->getCurrentWaypoint()->getRouteIndex(), + controller->announcePosition(getID(), fp.get(), fp->getCurrentWaypoint()->getRouteIndex(), _getLatitude(), _getLongitude(), hdg, speed, altitude_ft, trafficRef->getRadius(), leg, this); } @@ -650,7 +641,7 @@ cerr << "Error: Could not find Dynamics at airport : " << trafficRef->getDepartureAirport()->getId() << endl; } if (towerController) { - towerController->announcePosition(getID(), fp, fp->getCurrentWaypoint()->getRouteIndex(), + towerController->announcePosition(getID(), fp.get(), fp->getCurrentWaypoint()->getRouteIndex(), _getLatitude(), _getLongitude(), hdg, speed, altitude_ft, trafficRef->getRadius(), leg, this); //cerr << "Scheduling " << trafficRef->getCallSign() << " for takeoff " << endl; diff -Nru flightgear-2017.3.1+dfsg/src/AIModel/AIAircraft.hxx flightgear-2018.1.1+dfsg/src/AIModel/AIAircraft.hxx --- flightgear-2017.3.1+dfsg/src/AIModel/AIAircraft.hxx 2017-09-20 07:51:30.000000000 +0000 +++ flightgear-2018.1.1+dfsg/src/AIModel/AIAircraft.hxx 2018-04-06 19:43:33.000000000 +0000 @@ -49,9 +49,9 @@ // void setPerformance(PerformanceData *ps); void setFlightPlan(const std::string& fp, bool repat = false); - void SetFlightPlan(FGAIFlightPlan *f); + void initializeFlightPlan(); - FGAIFlightPlan* GetFlightPlan() const { return fp; }; + FGAIFlightPlan* GetFlightPlan() const { return fp.get(); }; void ProcessFlightPlan( double dt, time_t now ); time_t checkForArrivalTime(const std::string& wptName); diff -Nru flightgear-2017.3.1+dfsg/src/AIModel/AIBase.cxx flightgear-2018.1.1+dfsg/src/AIModel/AIBase.cxx --- flightgear-2017.3.1+dfsg/src/AIModel/AIBase.cxx 2017-09-20 07:51:30.000000000 +0000 +++ flightgear-2018.1.1+dfsg/src/AIModel/AIBase.cxx 2018-04-06 19:43:33.000000000 +0000 @@ -127,7 +127,6 @@ model_removed( fgGetNode("/ai/models/model-removed", true) ), manager( NULL ), _installed(false), - fp( NULL ), _impact_lat(0), _impact_lon(0), _impact_elev(0), @@ -205,10 +204,6 @@ } removeSoundFx(); - - if (fp) - delete fp; - fp = 0; } /** Cleanly remove the model @@ -935,7 +930,13 @@ return id; } -bool FGAIBase::isValid() { +void FGAIBase::setFlightPlan(std::unique_ptr f) +{ + fp = std::move(f); +} + +bool FGAIBase::isValid() const +{ //Either no flightplan or it is valid return !fp || fp->isValidPlan(); } @@ -950,3 +951,8 @@ return pos; } +void FGAIBase::setGeodPos(const SGGeod& geod) +{ + pos = geod; +} + diff -Nru flightgear-2017.3.1+dfsg/src/AIModel/AIBase.hxx flightgear-2018.1.1+dfsg/src/AIModel/AIBase.hxx --- flightgear-2017.3.1+dfsg/src/AIModel/AIBase.hxx 2017-09-20 07:51:30.000000000 +0000 +++ flightgear-2018.1.1+dfsg/src/AIModel/AIBase.hxx 2018-04-06 19:43:33.000000000 +0000 @@ -108,10 +108,13 @@ int _getSubID() const; bool getDie(); - bool isValid(); + bool isValid() const; + + void setFlightPlan(std::unique_ptr f); SGGeod getGeodPos() const; - + void setGeodPos(const SGGeod& pos); + SGVec3d getCartPosAt(const SGVec3d& off) const; SGVec3d getCartPos() const; @@ -211,7 +214,7 @@ double life; - FGAIFlightPlan *fp; + std::unique_ptr fp; bool _impact_reported; bool _collision_reported; diff -Nru flightgear-2017.3.1+dfsg/src/AIModel/AIFlightPlanCreate.cxx flightgear-2018.1.1+dfsg/src/AIModel/AIFlightPlanCreate.cxx --- flightgear-2017.3.1+dfsg/src/AIModel/AIFlightPlanCreate.cxx 2017-09-20 07:51:30.000000000 +0000 +++ flightgear-2018.1.1+dfsg/src/AIModel/AIFlightPlanCreate.cxx 2018-04-06 19:43:33.000000000 +0000 @@ -22,6 +22,8 @@ # include #endif +#include + #include #include @@ -409,7 +411,7 @@ // fallback mechanism for this. // Starting from gate 0 doesn't work, so don't try it FGTaxiRoute taxiRoute; - if (gate.isValid()) + if (runwayNode && gate.isValid()) taxiRoute = gn->findShortestRoute(runwayNode, gate.parking()); if (taxiRoute.empty()) { diff -Nru flightgear-2017.3.1+dfsg/src/AIModel/AIManager.cxx flightgear-2018.1.1+dfsg/src/AIModel/AIManager.cxx --- flightgear-2017.3.1+dfsg/src/AIModel/AIManager.cxx 2017-09-20 07:51:30.000000000 +0000 +++ flightgear-2018.1.1+dfsg/src/AIModel/AIManager.cxx 2018-04-06 19:43:33.000000000 +0000 @@ -28,9 +28,6 @@ #include #include -#include -#include - #include
#include
#include @@ -56,7 +53,7 @@ Scenario(FGAIManager* man, const std::string& nm, SGPropertyNode* scenarios) : _internalName(nm) { - BOOST_FOREACH(SGPropertyNode* scEntry, scenarios->getChildren("entry")) { + for (auto scEntry : scenarios->getChildren("entry")) { FGAIBasePtr ai = man->addObject(scEntry); if (ai) { _objects.push_back(ai); @@ -81,9 +78,9 @@ ~Scenario() { - BOOST_FOREACH(FGAIBasePtr ai, _objects) { - ai->setDie(true); - } + std::for_each(_objects.begin(), _objects.end(), + [](FGAIBasePtr ai) { ai->setDie(true); }); + FGNasalSys* nasalSys = (FGNasalSys*) globals->get_subsystem("nasal"); if (!nasalSys) @@ -117,7 +114,7 @@ FGAIManager::~FGAIManager() { - std::for_each(ai_list.begin(), ai_list.end(), boost::mem_fn(&FGAIBase::unbind)); + std::for_each(ai_list.begin(), ai_list.end(), std::mem_fn(&FGAIBase::unbind)); } void @@ -136,6 +133,16 @@ globals->get_commands()->addCommand("load-scenario", this, &FGAIManager::loadScenarioCommand); globals->get_commands()->addCommand("unload-scenario", this, &FGAIManager::unloadScenarioCommand); _environmentVisiblity = fgGetNode("/environment/visibility-m"); + + // Create an (invisible) AIAircraft representation of the current + // users's aircraft, that mimicks the user aircraft's behavior. + + _userAircraft = new FGAIAircraft; + _userAircraft->setCallSign ( fgGetString("/sim/multiplay/callsign") ); + _userAircraft->setGeodPos(globals->get_aircraft_position()); + _userAircraft->setPerformance("", "jet_transport"); + _userAircraft->setHeading(fgGetDouble("/orientation/heading-deg")); + _userAircraft->setSpeed(fgGetDouble("/velocities/groundspeed-kt")); } void @@ -148,7 +155,7 @@ enabled->setBoolValue(true); // process all scenarios - BOOST_FOREACH(SGPropertyNode* n, root->getChildren("scenario")) { + for (auto n : root->getChildren("scenario")) { const string& name = n->getStringValue(); if (name.empty()) continue; @@ -170,7 +177,7 @@ unloadAllScenarios(); update(0.0); - std::for_each(ai_list.begin(), ai_list.end(), boost::mem_fn(&FGAIBase::reinit)); + std::for_each(ai_list.begin(), ai_list.end(), std::mem_fn(&FGAIBase::reinit)); // (re-)load scenarios postinit(); @@ -181,7 +188,7 @@ { unloadAllScenarios(); - BOOST_FOREACH(FGAIBase* ai, ai_list) { + for (FGAIBase* ai : ai_list) { // other subsystems, especially ATC, may have references. This // lets them detect if the AI object should be skipped ai->setDie(true); @@ -190,6 +197,7 @@ ai_list.clear(); _environmentVisiblity.clear(); + _userAircraft.clear(); globals->get_commands()->removeCommand("load-scenario"); globals->get_commands()->removeCommand("unload-scenario"); @@ -223,7 +231,8 @@ } void -FGAIManager::update(double dt) { +FGAIManager::update(double dt) +{ // initialize these for finding nearest thermals range_nearest = 10000.0; strength = 0.0; @@ -231,14 +240,14 @@ if (!enabled->getBoolValue()) return; - fetchUserState(); + fetchUserState(dt); // partition the list into dead followed by alive - ai_list_iterator firstAlive = - std::stable_partition(ai_list.begin(), ai_list.end(), boost::mem_fn(&FGAIBase::getDie)); + auto firstAlive = + std::stable_partition(ai_list.begin(), ai_list.end(), std::mem_fn(&FGAIBase::getDie)); // clean up each item and finally remove from the container - for (ai_list_iterator it=ai_list.begin(); it != firstAlive; ++it) { + for (auto it=ai_list.begin(); it != firstAlive; ++it) { removeDeadItem(*it); } @@ -247,7 +256,7 @@ // every remaining item is alive. update them in turn, but guard for // exceptions, so a single misbehaving AI object doesn't bring down the // entire subsystem. - BOOST_FOREACH(FGAIBase* base, ai_list) { + for (FGAIBase* base : ai_list) { try { if (base->isa(FGAIBase::otThermal)) { processThermal(dt, (FGAIThermal*)base); @@ -269,7 +278,7 @@ FGAIManager::updateLOD(SGPropertyNode* node) { SG_UNUSED(node); - std::for_each(ai_list.begin(), ai_list.end(), boost::mem_fn(&FGAIBase::updateLOD)); + std::for_each(ai_list.begin(), ai_list.end(), std::mem_fn(&FGAIBase::updateLOD)); } void @@ -317,14 +326,19 @@ } void -FGAIManager::fetchUserState( void ) { +FGAIManager::fetchUserState( double dt ) +{ globals->get_aircraft_orientation(user_heading, user_pitch, user_roll); user_speed = user_speed_node->getDoubleValue() * 0.592484; wind_from_east = wind_from_east_node->getDoubleValue(); wind_from_north = wind_from_north_node->getDoubleValue(); user_altitude_agl = user_altitude_agl_node->getDoubleValue(); - + + _userAircraft->setGeodPos(globals->get_aircraft_position()); + _userAircraft->setHeading(user_heading); + _userAircraft->setSpeed(fgGetDouble("/velocities/groundspeed-kt")); + _userAircraft->update(dt); } // only keep the results from the nearest thermal @@ -426,7 +440,7 @@ bool FGAIManager::removeObject(const SGPropertyNode* args) { int id = args->getIntValue("id"); - BOOST_FOREACH(FGAIBase* ai, get_ai_list()) { + for (FGAIBase* ai : get_ai_list()) { if (ai->getID() == id) { ai->setDie(true); break; @@ -438,13 +452,12 @@ FGAIBasePtr FGAIManager::getObjectFromProperty(const SGPropertyNode* aProp) const { - BOOST_FOREACH(FGAIBase* ai, get_ai_list()) { - if (ai->_getProps() == aProp) { - return ai; - } - } // of AI objects iteration - - return NULL; + auto it = std::find_if(ai_list.begin(), ai_list.end(), + [aProp](FGAIBasePtr ai) { return ai->_getProps() == aProp; }); + if (it == ai_list.end()) { + return nullptr; + } + return *it; } bool @@ -583,4 +596,9 @@ return distM * SG_METER_TO_FEET; } +FGAIAircraft* FGAIManager::getUserAircraft() const +{ + return _userAircraft.get(); +} + //end AIManager.cxx diff -Nru flightgear-2017.3.1+dfsg/src/AIModel/AIManager.hxx flightgear-2018.1.1+dfsg/src/AIModel/AIManager.hxx --- flightgear-2017.3.1+dfsg/src/AIModel/AIManager.hxx 2017-09-20 07:51:30.000000000 +0000 +++ flightgear-2018.1.1+dfsg/src/AIModel/AIManager.hxx 2018-04-06 19:43:33.000000000 +0000 @@ -31,6 +31,7 @@ class FGAIBase; class FGAIThermal; +class FGAIAircraft; typedef SGSharedPtr FGAIBasePtr; @@ -74,7 +75,7 @@ */ FGAIBasePtr getObjectFromProperty(const SGPropertyNode* aProp) const; - typedef std::list ai_list_type; + typedef std::vector ai_list_type; const ai_list_type& get_ai_list() const { return ai_list; } @@ -82,6 +83,14 @@ double calcRangeFt(const SGVec3d& aCartPos, const FGAIBase* aObject) const; static const char* subsystemName() { return "ai-model"; } + + /** + * @brief Retrieve the representation of the user's aircraft in the AI manager + * the position and velocity of this object are slaved to the user's aircraft, + * so that AI systems such as parking and ATC can see the user and process / + * avoid correctly. + */ + FGAIAircraft* getUserAircraft() const; private: // FGSubmodelMgr is a friend for access to the AI_list friend class FGSubmodelMgr; @@ -122,7 +131,7 @@ double wind_from_east; double wind_from_north; - void fetchUserState( void ); + void fetchUserState( double dt ); // used by thermals double range_nearest; @@ -135,6 +144,8 @@ class Scenario; typedef std::map ScenarioDict; ScenarioDict _scenarios; + + SGSharedPtr _userAircraft; }; #endif // _FG_AIMANAGER_HXX diff -Nru flightgear-2017.3.1+dfsg/src/AIModel/AIMultiplayer.cxx flightgear-2018.1.1+dfsg/src/AIModel/AIMultiplayer.cxx --- flightgear-2017.3.1+dfsg/src/AIModel/AIMultiplayer.cxx 2017-09-20 07:51:30.000000000 +0000 +++ flightgear-2018.1.1+dfsg/src/AIModel/AIMultiplayer.cxx 2018-04-06 19:43:33.000000000 +0000 @@ -30,6 +30,7 @@ #include
#include
+#include