| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| { | ||
| "app-id": "io.github.Hexchat", | ||
| "branch": "stable", | ||
| "runtime": "org.gnome.Platform", | ||
| "runtime-version": "40", | ||
| "sdk": "org.gnome.Sdk", | ||
| "command": "hexchat", | ||
| "rename-icon": "hexchat", | ||
| "finish-args": [ | ||
| "--share=ipc", | ||
| "--socket=x11", | ||
| "--share=network", | ||
| "--socket=pulseaudio", | ||
| "--filesystem=xdg-download", | ||
|
|
||
| "--talk-name=org.freedesktop.Notifications", | ||
|
|
||
| "--talk-name=org.mpris.MediaPlayer2.*" | ||
| ], | ||
| "add-extensions": { | ||
| "io.github.Hexchat.Plugin": { | ||
| "version": "20.08", | ||
| "directory": "extensions", | ||
| "add-ld-path": "lib", | ||
| "merge-dirs": "lib/hexchat/plugins", | ||
| "subdirectories": true, | ||
| "no-autodownload": true, | ||
| "autodelete": true | ||
| } | ||
| }, | ||
| "modules": [ | ||
| "shared-modules/gtk2/gtk2.json", | ||
| "shared-modules/gtk2/gtk2-common-themes.json", | ||
| "shared-modules/dbus-glib/dbus-glib-0.110.json", | ||
| "shared-modules/lua5.3/lua-5.3.5.json", | ||
| "shared-modules/libcanberra/libcanberra.json", | ||
| "python3-cffi.json", | ||
| { | ||
| "name": "lgi", | ||
| "buildsystem": "meson", | ||
| "sources": [ | ||
| { | ||
| "type": "git", | ||
| "url": "https://github.com/pavouk/lgi.git", | ||
| "commit": "95418635aa8151a516d43166227ea2b9d4c4403f" | ||
| } | ||
| ] | ||
| }, | ||
| { | ||
| "name": "hexchat", | ||
| "buildsystem": "meson", | ||
| "config-opts": [ | ||
| "--buildtype=release", | ||
| "-Ddbus-service-use-appid=true", | ||
| "-Dwith-perl=false", | ||
| "-Dwith-lua=lua" | ||
| ], | ||
| "build-options": { | ||
| "cflags": "-Wno-error=missing-include-dirs" | ||
| }, | ||
| "cleanup": [ | ||
| "/share/man" | ||
| ], | ||
| "post-install": [ | ||
| "install -d /app/extensions" | ||
| ], | ||
| "sources": [ | ||
| { | ||
| "type": "dir", | ||
| "path": ".." | ||
| }, | ||
| { | ||
| "type": "patch", | ||
| "path": "Load-plugins-from-Flatpak-extensions.patch" | ||
| } | ||
| ] | ||
| } | ||
| ] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| { | ||
| "name": "python3-cffi", | ||
| "buildsystem": "simple", | ||
| "build-commands": [ | ||
| "pip3 install --verbose --exists-action=i --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} \"cffi\" --no-build-isolation" | ||
| ], | ||
| "sources": [ | ||
| { | ||
| "type": "file", | ||
| "url": "https://files.pythonhosted.org/packages/0f/86/e19659527668d70be91d0369aeaa055b4eb396b0f387a4f92293a20035bd/pycparser-2.20.tar.gz", | ||
| "sha256": "2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0" | ||
| }, | ||
| { | ||
| "type": "file", | ||
| "url": "https://files.pythonhosted.org/packages/a8/20/025f59f929bbcaa579704f443a438135918484fffaacfaddba776b374563/cffi-1.14.5.tar.gz", | ||
| "sha256": "fd78e5fee591709f32ef6edb9a015b4aa1a5022598e36227500c8f4e02328d9c" | ||
| } | ||
| ] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| fishlim_test_sources = [ | ||
| 'tests.c', | ||
| 'mock-keystore.c', | ||
| '../fish.c', | ||
| '../utils.c', | ||
| ] | ||
|
|
||
| fishlim_tests = executable('fishlim_tests', fishlim_test_sources, | ||
| dependencies: [libgio_dep, libssl_dep, hexchat_plugin_dep], | ||
| include_directories: include_directories('..'), | ||
| ) | ||
|
|
||
| test('Fishlim Tests', fishlim_tests, | ||
| protocol: 'tap', | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| /* | ||
| Copyright (c) 2010 Samuel Lidén Borell <samuel@kodafritt.se> | ||
| Permission is hereby granted, free of charge, to any person obtaining a copy | ||
| of this software and associated documentation files (the "Software"), to deal | ||
| in the Software without restriction, including without limitation the rights | ||
| to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
| copies of the Software, and to permit persons to whom the Software is | ||
| furnished to do so, subject to the following conditions: | ||
| The above copyright notice and this permission notice shall be included in | ||
| all copies or substantial portions of the Software. | ||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
| FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
| AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
| LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
| OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | ||
| THE SOFTWARE. | ||
| */ | ||
|
|
||
| #include "fish.h" | ||
|
|
||
| /** | ||
| * Extracts a key from the key store file. | ||
| */ | ||
| char * | ||
| keystore_get_key(const char *nick, enum fish_mode *mode) | ||
| { | ||
| return NULL; | ||
| } | ||
|
|
||
| /** | ||
| * Sets a key in the key store file. | ||
| */ | ||
| gboolean | ||
| keystore_store_key(const char *nick, const char *key, enum fish_mode mode) | ||
| { | ||
| return TRUE; | ||
| } | ||
|
|
||
| /** | ||
| * Deletes a nick from the key store. | ||
| */ | ||
| gboolean | ||
| keystore_delete_nick(const char *nick) | ||
| { | ||
| return TRUE; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,286 @@ | ||
| /* | ||
| Copyright (c) 2020 <bakasura@protonmail.ch> | ||
| Permission is hereby granted, free of charge, to any person obtaining a copy | ||
| of this software and associated documentation files (the "Software"), to deal | ||
| in the Software without restriction, including without limitation the rights | ||
| to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
| copies of the Software, and to permit persons to whom the Software is | ||
| furnished to do so, subject to the following conditions: | ||
| The above copyright notice and this permission notice shall be included in | ||
| all copies or substantial portions of the Software. | ||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
| FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
| AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
| LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
| OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | ||
| THE SOFTWARE. | ||
| */ | ||
|
|
||
| #include <glib.h> | ||
|
|
||
| #include "fish.h" | ||
| #include "utils.h" | ||
|
|
||
| /** | ||
| * Auxiliary function: Generate a random string | ||
| * @param out Preallocated string to fill | ||
| * @param len Size of bytes to fill | ||
| */ | ||
| static void | ||
| random_string(char *out, size_t len) | ||
| { | ||
| GRand *rand = NULL; | ||
| int i = 0; | ||
|
|
||
| rand = g_rand_new(); | ||
| for (i = 0; i < len; ++i) { | ||
| out[i] = g_rand_int_range(rand, 1, 256); | ||
| } | ||
|
|
||
| out[len] = 0; | ||
|
|
||
| g_rand_free(rand); | ||
| } | ||
|
|
||
| /** | ||
| * Check encrypt and decrypt in ECB mode | ||
| */ | ||
| static void | ||
| test_ecb(void) | ||
| { | ||
| char *b64 = NULL; | ||
| char *de = NULL; | ||
| int key_len, message_len = 0; | ||
| char key[57]; | ||
| char message[1000]; | ||
|
|
||
| /* Generate key 32–448 bits (Yes, I start with 8 bits) */ | ||
| for (key_len = 1; key_len < 57; ++key_len) { | ||
|
|
||
| random_string(key, key_len); | ||
|
|
||
| for (message_len = 1; message_len < 1000; ++message_len) { | ||
| random_string(message, message_len); | ||
|
|
||
| /* Encrypt */ | ||
| b64 = fish_encrypt(key, key_len, message, message_len, FISH_ECB_MODE); | ||
| g_assert_nonnull(b64); | ||
|
|
||
| /* Decrypt */ | ||
| /* Linear */ | ||
| de = fish_decrypt_str(key, key_len, b64, FISH_ECB_MODE); | ||
| g_assert_cmpstr (de, ==, message); | ||
| g_free(de); | ||
|
|
||
| /* Mixed */ | ||
| de = fish_decrypt_str(key, key_len, b64, FISH_ECB_MODE); | ||
| g_assert_cmpstr (de, ==, message); | ||
| g_free(de); | ||
|
|
||
| g_free(b64); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Check encrypt and decrypt in CBC mode | ||
| */ | ||
| static void | ||
| test_cbc(void) | ||
| { | ||
| char *b64 = NULL; | ||
| char *de = NULL; | ||
| int key_len, message_len = 0; | ||
| char key[57]; | ||
| char message[1000]; | ||
|
|
||
| /* Generate key 32–448 bits (Yes, I start with 8 bits) */ | ||
| for (key_len = 1; key_len < 57; ++key_len) { | ||
|
|
||
| random_string(key, key_len); | ||
|
|
||
| for (message_len = 1; message_len < 1000; ++message_len) { | ||
| random_string(message, message_len); | ||
|
|
||
| /* Encrypt */ | ||
| b64 = fish_encrypt(key, key_len, message, message_len, FISH_CBC_MODE); | ||
| g_assert_nonnull(b64); | ||
|
|
||
| /* Decrypt */ | ||
| /* Linear */ | ||
| de = fish_decrypt_str(key, key_len, b64, FISH_CBC_MODE); | ||
| g_assert_cmpstr (de, ==, message); | ||
| g_free(de); | ||
|
|
||
| g_free(b64); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Check the calculation of final length from an encoded string in Base64 | ||
| */ | ||
| static void | ||
| test_base64_len (void) | ||
| { | ||
| char *b64 = NULL; | ||
| int i, message_len = 0; | ||
| char message[1000]; | ||
|
|
||
| for (i = 0; i < 10; ++i) { | ||
| for (message_len = 1; message_len < 1000; ++message_len) { | ||
| random_string(message, message_len); | ||
| b64 = g_base64_encode((const unsigned char *) message, message_len); | ||
| g_assert_nonnull(b64); | ||
| g_assert_cmpuint(strlen(b64), == , base64_len(message_len)); | ||
| g_free(b64); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Check the calculation of final length from an encoded string in BlowcryptBase64 | ||
| */ | ||
| static void | ||
| test_base64_fish_len (void) | ||
| { | ||
| char *b64 = NULL; | ||
| int i, message_len = 0; | ||
| char message[1000]; | ||
|
|
||
| for (i = 0; i < 10; ++i) { | ||
|
|
||
| for (message_len = 1; message_len < 1000; ++message_len) { | ||
| random_string(message, message_len); | ||
| b64 = fish_base64_encode(message, message_len); | ||
| g_assert_nonnull(b64); | ||
| g_assert_cmpuint(strlen(b64), == , base64_fish_len(message_len)); | ||
| g_free(b64); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Check the calculation of final length from an encrypted string in ECB mode | ||
| */ | ||
| static void | ||
| test_base64_ecb_len(void) | ||
| { | ||
| char *b64 = NULL; | ||
| int key_len, message_len = 0; | ||
| char key[57]; | ||
| char message[1000]; | ||
|
|
||
| /* Generate key 32–448 bits (Yes, I start with 8 bits) */ | ||
| for (key_len = 1; key_len < 57; ++key_len) { | ||
|
|
||
| random_string(key, key_len); | ||
|
|
||
| for (message_len = 1; message_len < 1000; ++message_len) { | ||
| random_string(message, message_len); | ||
| b64 = fish_encrypt(key, key_len, message, message_len, FISH_ECB_MODE); | ||
| g_assert_nonnull(b64); | ||
| g_assert_cmpuint(strlen(b64), == , ecb_len(message_len)); | ||
| g_free(b64); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Check the calculation of final length from an encrypted string in CBC mode | ||
| */ | ||
| static void | ||
| test_base64_cbc_len(void) | ||
| { | ||
| char *b64 = NULL; | ||
| int key_len, message_len = 0; | ||
| char key[57]; | ||
| char message[1000]; | ||
|
|
||
| /* Generate key 32–448 bits (Yes, I start with 8 bits) */ | ||
| for (key_len = 1; key_len < 57; ++key_len) { | ||
|
|
||
| random_string(key, key_len); | ||
|
|
||
| for (message_len = 1; message_len < 1000; ++message_len) { | ||
| random_string(message, message_len); | ||
| b64 = fish_encrypt(key, key_len, message, message_len, FISH_CBC_MODE); | ||
| g_assert_nonnull(b64); | ||
| g_assert_cmpuint(strlen(b64), == , cbc_len(message_len)); | ||
| g_free(b64); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Check the calculation of length limit for a plaintext in each encryption mode | ||
| */ | ||
| static void | ||
| test_max_text_command_len(void) | ||
| { | ||
| int max_encoded_len, plaintext_len; | ||
| enum fish_mode mode; | ||
|
|
||
| for (max_encoded_len = 0; max_encoded_len < 10000; ++max_encoded_len) { | ||
| for (mode = FISH_ECB_MODE; mode <= FISH_CBC_MODE; ++mode) { | ||
| plaintext_len = max_text_command_len(max_encoded_len, mode); | ||
| g_assert_cmpuint(encoded_len(plaintext_len, mode), <= , max_encoded_len); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Check the calculation of length limit for a plaintext in each encryption mode | ||
| */ | ||
| static void | ||
| test_foreach_utf8_data_chunks(void) | ||
| { | ||
| GRand *rand = NULL; | ||
| GString *chunks = NULL; | ||
| int tests, max_chunks_len, chunks_len; | ||
| char ascii_message[1001]; | ||
| char *data_chunk = NULL; | ||
|
|
||
| rand = g_rand_new(); | ||
|
|
||
| for (tests = 0; tests < 1000; ++tests) { | ||
|
|
||
| max_chunks_len = g_rand_int_range(rand, 2, 301); | ||
| random_string(ascii_message, 1000); | ||
|
|
||
| data_chunk = ascii_message; | ||
|
|
||
| chunks = g_string_new(NULL); | ||
|
|
||
| while (foreach_utf8_data_chunks(data_chunk, max_chunks_len, &chunks_len)) { | ||
| g_string_append(chunks, g_strndup(data_chunk, chunks_len)); | ||
| /* Next chunk */ | ||
| data_chunk += chunks_len; | ||
| } | ||
| /* Check data loss */ | ||
| g_assert_cmpstr(chunks->str, == , ascii_message); | ||
| g_string_free(chunks, TRUE); | ||
| } | ||
| } | ||
|
|
||
| int | ||
| main(int argc, char *argv[]) { | ||
|
|
||
| g_test_init(&argc, &argv, NULL); | ||
|
|
||
| g_test_add_func("/fishlim/ecb", test_ecb); | ||
| g_test_add_func("/fishlim/cbc", test_cbc); | ||
| g_test_add_func("/fishlim/base64_len", test_base64_len); | ||
| g_test_add_func("/fishlim/base64_fish_len", test_base64_fish_len); | ||
| g_test_add_func("/fishlim/base64_ecb_len", test_base64_ecb_len); | ||
| g_test_add_func("/fishlim/base64_cbc_len", test_base64_cbc_len); | ||
| g_test_add_func("/fishlim/max_text_command_len", test_max_text_command_len); | ||
| g_test_add_func("/fishlim/foreach_utf8_data_chunks", test_foreach_utf8_data_chunks); | ||
|
|
||
| return g_test_run(); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,149 @@ | ||
| /* | ||
| Copyright (c) 2020 <bakasura@protonmail.ch> | ||
| Permission is hereby granted, free of charge, to any person obtaining a copy | ||
| of this software and associated documentation files (the "Software"), to deal | ||
| in the Software without restriction, including without limitation the rights | ||
| to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
| copies of the Software, and to permit persons to whom the Software is | ||
| furnished to do so, subject to the following conditions: | ||
| The above copyright notice and this permission notice shall be included in | ||
| all copies or substantial portions of the Software. | ||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
| FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
| AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
| LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
| OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | ||
| THE SOFTWARE. | ||
| */ | ||
|
|
||
| #include "utils.h" | ||
| #include "fish.h" | ||
|
|
||
| /** | ||
| * Calculate the length of Base64-encoded string | ||
| * | ||
| * @param plaintext_len Size of clear text to encode | ||
| * @return Size of encoded string | ||
| */ | ||
| unsigned long base64_len(size_t plaintext_len) { | ||
| int length_unpadded = (4 * plaintext_len) / 3; | ||
| /* Add padding */ | ||
| return length_unpadded % 4 != 0 ? length_unpadded + (4 - length_unpadded % 4) : length_unpadded; | ||
| } | ||
|
|
||
| /** | ||
| * Calculate the length of BlowcryptBase64-encoded string | ||
| * | ||
| * @param plaintext_len Size of clear text to encode | ||
| * @return Size of encoded string | ||
| */ | ||
| unsigned long base64_fish_len(size_t plaintext_len) { | ||
| int length_unpadded = (12 * plaintext_len) / 8; | ||
| /* Add padding */ | ||
| return length_unpadded % 12 != 0 ? length_unpadded + (12 - length_unpadded % 12) : length_unpadded; | ||
| } | ||
|
|
||
| /** | ||
| * Calculate the length of fish-encrypted string in CBC mode | ||
| * | ||
| * @param plaintext_len Size of clear text to encode | ||
| * @return Size of encoded string | ||
| */ | ||
| unsigned long cbc_len(size_t plaintext_len) { | ||
| /*IV + DATA + Zero Padding */ | ||
| return base64_len(8 + (plaintext_len % 8 != 0 ? plaintext_len + 8 - (plaintext_len % 8) : plaintext_len)); | ||
| } | ||
|
|
||
| /** | ||
| * Calculate the length of fish-encrypted string in ECB mode | ||
| * | ||
| * @param plaintext_len Size of clear text to encode | ||
| * @return Size of encoded string | ||
| */ | ||
| unsigned long ecb_len(size_t plaintext_len) { | ||
| return base64_fish_len(plaintext_len); | ||
| } | ||
|
|
||
| /** | ||
| * Calculate the length of encrypted string in 'mode' mode | ||
| * | ||
| * @param plaintext_len Length of plaintext | ||
| * @param mode Encryption mode | ||
| * @return Size of encoded string | ||
| */ | ||
| unsigned long encoded_len(size_t plaintext_len, enum fish_mode mode) { | ||
| switch (mode) { | ||
|
|
||
| case FISH_CBC_MODE: | ||
| return cbc_len(plaintext_len); | ||
| break; | ||
|
|
||
| case FISH_ECB_MODE: | ||
| return ecb_len(plaintext_len); | ||
| } | ||
|
|
||
| return 0; | ||
| } | ||
|
|
||
| /** | ||
| * Determine the maximum length of plaintext for a 'max_len' limit taking care the overload of encryption | ||
| * | ||
| * @param max_len Limit for plaintext | ||
| * @param mode Encryption mode | ||
| * @return Maximum allowed plaintext length | ||
| */ | ||
| int max_text_command_len(size_t max_len, enum fish_mode mode) { | ||
| int len; | ||
|
|
||
| for (len = max_len; encoded_len(len, mode) > max_len; --len); | ||
| return len; | ||
| } | ||
|
|
||
| /** | ||
| * Iterate over 'data' in chunks of 'max_chunk_len' taking care the UTF-8 characters | ||
| * | ||
| * @param data Data to iterate | ||
| * @param max_chunk_len Size of biggest chunk | ||
| * @param [out] chunk_len Current chunk length | ||
| * @return Pointer to current chunk position or NULL if not have more chunks | ||
| */ | ||
| const char *foreach_utf8_data_chunks(const char *data, int max_chunk_len, int *chunk_len) { | ||
| int data_len, last_chunk_len = 0; | ||
|
|
||
| if (!*data) { | ||
| return NULL; | ||
| } | ||
|
|
||
| /* Last chunk of data */ | ||
| data_len = strlen(data); | ||
| if (data_len <= max_chunk_len) { | ||
| *chunk_len = data_len; | ||
| return data; | ||
| } | ||
|
|
||
| *chunk_len = 0; | ||
| const char *utf8_character = data; | ||
|
|
||
| /* Not valid UTF-8, but maybe valid text, just split into max length */ | ||
| if (!g_utf8_validate(data, -1, NULL)) { | ||
| *chunk_len = max_chunk_len; | ||
| return utf8_character; | ||
| } | ||
|
|
||
| while (*utf8_character && *chunk_len <= max_chunk_len) { | ||
| last_chunk_len = *chunk_len; | ||
| *chunk_len = (g_utf8_next_char(utf8_character) - data) * sizeof(*utf8_character); | ||
| utf8_character = g_utf8_next_char(utf8_character); | ||
| } | ||
|
|
||
| /* We need the previous length before overflow the limit */ | ||
| *chunk_len = last_chunk_len; | ||
|
|
||
| return utf8_character; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| /* | ||
| Copyright (c) 2020 <bakasura@protonmail.ch> | ||
| Permission is hereby granted, free of charge, to any person obtaining a copy | ||
| of this software and associated documentation files (the "Software"), to deal | ||
| in the Software without restriction, including without limitation the rights | ||
| to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
| copies of the Software, and to permit persons to whom the Software is | ||
| furnished to do so, subject to the following conditions: | ||
| The above copyright notice and this permission notice shall be included in | ||
| all copies or substantial portions of the Software. | ||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
| FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
| AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
| LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
| OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | ||
| THE SOFTWARE. | ||
| */ | ||
|
|
||
| #ifndef PLUGIN_HEXCHAT_FISHLIM_UTILS_H | ||
| #define PLUGIN_HEXCHAT_FISHLIM_UTILS_H | ||
|
|
||
| #include <stddef.h> | ||
| #include "fish.h" | ||
|
|
||
| unsigned long base64_len(size_t plaintext_len); | ||
| unsigned long base64_fish_len(size_t plaintext_len); | ||
| unsigned long cbc_len(size_t plaintext_len); | ||
| unsigned long ecb_len(size_t plaintext_len); | ||
| unsigned long encoded_len(size_t plaintext_len, enum fish_mode mode); | ||
| int max_text_command_len(size_t max_len, enum fish_mode mode); | ||
| const char *foreach_utf8_data_chunks(const char *data, int max_chunk_len, int *chunk_len); | ||
|
|
||
| #endif |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,386 @@ | ||
| import inspect | ||
| import sys | ||
| from contextlib import contextmanager | ||
|
|
||
| from _hexchat_embedded import ffi, lib | ||
|
|
||
| __all__ = [ | ||
| 'EAT_ALL', 'EAT_HEXCHAT', 'EAT_NONE', 'EAT_PLUGIN', 'EAT_XCHAT', | ||
| 'PRI_HIGH', 'PRI_HIGHEST', 'PRI_LOW', 'PRI_LOWEST', 'PRI_NORM', | ||
| '__doc__', '__version__', 'command', 'del_pluginpref', 'emit_print', | ||
| 'find_context', 'get_context', 'get_info', | ||
| 'get_list', 'get_lists', 'get_pluginpref', 'get_prefs', 'hook_command', | ||
| 'hook_print', 'hook_print_attrs', 'hook_server', 'hook_server_attrs', | ||
| 'hook_timer', 'hook_unload', 'list_pluginpref', 'nickcmp', 'prnt', | ||
| 'set_pluginpref', 'strip', 'unhook', | ||
| ] | ||
|
|
||
| __doc__ = 'HexChat Scripting Interface' | ||
| __version__ = (2, 0) | ||
| __license__ = 'GPL-2.0+' | ||
|
|
||
| EAT_NONE = 0 | ||
| EAT_HEXCHAT = 1 | ||
| EAT_XCHAT = EAT_HEXCHAT | ||
| EAT_PLUGIN = 2 | ||
| EAT_ALL = EAT_HEXCHAT | EAT_PLUGIN | ||
|
|
||
| PRI_LOWEST = -128 | ||
| PRI_LOW = -64 | ||
| PRI_NORM = 0 | ||
| PRI_HIGH = 64 | ||
| PRI_HIGHEST = 127 | ||
|
|
||
|
|
||
| # We need each module to be able to reference their parent plugin | ||
| # which is a bit tricky since they all share the exact same module. | ||
| # Simply navigating up to what module called it seems to actually | ||
| # be a fairly reliable and simple method of doing so if ugly. | ||
| def __get_current_plugin(): | ||
| frame = inspect.stack()[1][0] | ||
| while '__plugin' not in frame.f_globals: | ||
| frame = frame.f_back | ||
| assert frame is not None | ||
|
|
||
| return frame.f_globals['__plugin'] | ||
|
|
||
|
|
||
| # Keeping API compat | ||
| if sys.version_info[0] == 2: | ||
| def __decode(string): | ||
| return string | ||
|
|
||
| else: | ||
| def __decode(string): | ||
| return string.decode() | ||
|
|
||
|
|
||
| # ------------ API ------------ | ||
| def prnt(string): | ||
| lib.hexchat_print(lib.ph, string.encode()) | ||
|
|
||
|
|
||
| def emit_print(event_name, *args, **kwargs): | ||
| time = kwargs.pop('time', 0) # For py2 compat | ||
| cargs = [] | ||
| for i in range(4): | ||
| arg = args[i].encode() if len(args) > i else b'' | ||
| cstring = ffi.new('char[]', arg) | ||
| cargs.append(cstring) | ||
|
|
||
| if time == 0: | ||
| return lib.hexchat_emit_print(lib.ph, event_name.encode(), *cargs) | ||
|
|
||
| attrs = lib.hexchat_event_attrs_create(lib.ph) | ||
| attrs.server_time_utc = time | ||
| ret = lib.hexchat_emit_print_attrs(lib.ph, attrs, event_name.encode(), *cargs) | ||
| lib.hexchat_event_attrs_free(lib.ph, attrs) | ||
| return ret | ||
|
|
||
|
|
||
| # TODO: this shadows itself. command should be changed to cmd | ||
| def command(command): | ||
| lib.hexchat_command(lib.ph, command.encode()) | ||
|
|
||
|
|
||
| def nickcmp(string1, string2): | ||
| return lib.hexchat_nickcmp(lib.ph, string1.encode(), string2.encode()) | ||
|
|
||
|
|
||
| def strip(text, length=-1, flags=3): | ||
| stripped = lib.hexchat_strip(lib.ph, text.encode(), length, flags) | ||
| ret = __decode(ffi.string(stripped)) | ||
| lib.hexchat_free(lib.ph, stripped) | ||
| return ret | ||
|
|
||
|
|
||
| def get_info(name): | ||
| ret = lib.hexchat_get_info(lib.ph, name.encode()) | ||
| if ret == ffi.NULL: | ||
| return None | ||
| if name in ('gtkwin_ptr', 'win_ptr'): | ||
| # Surely there is a less dumb way? | ||
| ptr = repr(ret).rsplit(' ', 1)[1][:-1] | ||
| return ptr | ||
|
|
||
| return __decode(ffi.string(ret)) | ||
|
|
||
|
|
||
| def get_prefs(name): | ||
| string_out = ffi.new('char**') | ||
| int_out = ffi.new('int*') | ||
| _type = lib.hexchat_get_prefs(lib.ph, name.encode(), string_out, int_out) | ||
| if _type == 0: | ||
| return None | ||
|
|
||
| if _type == 1: | ||
| return __decode(ffi.string(string_out[0])) | ||
|
|
||
| if _type in (2, 3): # XXX: 3 should be a bool, but keeps API | ||
| return int_out[0] | ||
|
|
||
| raise AssertionError('Out of bounds pref storage') | ||
|
|
||
|
|
||
| def __cstrarray_to_list(arr): | ||
| i = 0 | ||
| ret = [] | ||
| while arr[i] != ffi.NULL: | ||
| ret.append(ffi.string(arr[i])) | ||
| i += 1 | ||
|
|
||
| return ret | ||
|
|
||
|
|
||
| __FIELD_CACHE = {} | ||
|
|
||
|
|
||
| def __get_fields(name): | ||
| return __FIELD_CACHE.setdefault(name, __cstrarray_to_list(lib.hexchat_list_fields(lib.ph, name))) | ||
|
|
||
|
|
||
| __FIELD_PROPERTY_CACHE = {} | ||
|
|
||
|
|
||
| def __cached_decoded_str(string): | ||
| return __FIELD_PROPERTY_CACHE.setdefault(string, __decode(string)) | ||
|
|
||
|
|
||
| def get_lists(): | ||
| return [__cached_decoded_str(field) for field in __get_fields(b'lists')] | ||
|
|
||
|
|
||
| class ListItem: | ||
| def __init__(self, name): | ||
| self._listname = name | ||
|
|
||
| def __repr__(self): | ||
| return '<{} list item at {}>'.format(self._listname, id(self)) | ||
|
|
||
|
|
||
| # done this way for speed | ||
| if sys.version_info[0] == 2: | ||
| def get_getter(name): | ||
| return ord(name[0]) | ||
|
|
||
| else: | ||
| def get_getter(name): | ||
| return name[0] | ||
|
|
||
|
|
||
| def get_list(name): | ||
| # XXX: This function is extremely inefficient and could be interators and | ||
| # lazily loaded properties, but for API compat we stay slow | ||
| orig_name = name | ||
| name = name.encode() | ||
|
|
||
| if name not in __get_fields(b'lists'): | ||
| raise KeyError('list not available') | ||
|
|
||
| list_ = lib.hexchat_list_get(lib.ph, name) | ||
| if list_ == ffi.NULL: | ||
| return None | ||
|
|
||
| ret = [] | ||
| fields = __get_fields(name) | ||
|
|
||
| def string_getter(field): | ||
| string = lib.hexchat_list_str(lib.ph, list_, field) | ||
| if string != ffi.NULL: | ||
| return __decode(ffi.string(string)) | ||
|
|
||
| return '' | ||
|
|
||
| def ptr_getter(field): | ||
| if field == b'context': | ||
| ptr = lib.hexchat_list_str(lib.ph, list_, field) | ||
| ctx = ffi.cast('hexchat_context*', ptr) | ||
| return Context(ctx) | ||
|
|
||
| return None | ||
|
|
||
| getters = { | ||
| ord('s'): string_getter, | ||
| ord('i'): lambda field: lib.hexchat_list_int(lib.ph, list_, field), | ||
| ord('t'): lambda field: lib.hexchat_list_time(lib.ph, list_, field), | ||
| ord('p'): ptr_getter, | ||
| } | ||
|
|
||
| while lib.hexchat_list_next(lib.ph, list_) == 1: | ||
| item = ListItem(orig_name) | ||
| for _field in fields: | ||
| getter = getters.get(get_getter(_field)) | ||
| if getter is not None: | ||
| field_name = _field[1:] | ||
| setattr(item, __cached_decoded_str(field_name), getter(field_name)) | ||
|
|
||
| ret.append(item) | ||
|
|
||
| lib.hexchat_list_free(lib.ph, list_) | ||
| return ret | ||
|
|
||
|
|
||
| # TODO: 'command' here shadows command above, and should be renamed to cmd | ||
| def hook_command(command, callback, userdata=None, priority=PRI_NORM, help=None): | ||
| plugin = __get_current_plugin() | ||
| hook = plugin.add_hook(callback, userdata) | ||
| handle = lib.hexchat_hook_command(lib.ph, command.encode(), priority, lib._on_command_hook, | ||
| help.encode() if help is not None else ffi.NULL, hook.handle) | ||
|
|
||
| hook.hexchat_hook = handle | ||
| return id(hook) | ||
|
|
||
|
|
||
| def hook_print(name, callback, userdata=None, priority=PRI_NORM): | ||
| plugin = __get_current_plugin() | ||
| hook = plugin.add_hook(callback, userdata) | ||
| handle = lib.hexchat_hook_print(lib.ph, name.encode(), priority, lib._on_print_hook, hook.handle) | ||
| hook.hexchat_hook = handle | ||
| return id(hook) | ||
|
|
||
|
|
||
| def hook_print_attrs(name, callback, userdata=None, priority=PRI_NORM): | ||
| plugin = __get_current_plugin() | ||
| hook = plugin.add_hook(callback, userdata) | ||
| handle = lib.hexchat_hook_print_attrs(lib.ph, name.encode(), priority, lib._on_print_attrs_hook, hook.handle) | ||
| hook.hexchat_hook = handle | ||
| return id(hook) | ||
|
|
||
|
|
||
| def hook_server(name, callback, userdata=None, priority=PRI_NORM): | ||
| plugin = __get_current_plugin() | ||
| hook = plugin.add_hook(callback, userdata) | ||
| handle = lib.hexchat_hook_server(lib.ph, name.encode(), priority, lib._on_server_hook, hook.handle) | ||
| hook.hexchat_hook = handle | ||
| return id(hook) | ||
|
|
||
|
|
||
| def hook_server_attrs(name, callback, userdata=None, priority=PRI_NORM): | ||
| plugin = __get_current_plugin() | ||
| hook = plugin.add_hook(callback, userdata) | ||
| handle = lib.hexchat_hook_server_attrs(lib.ph, name.encode(), priority, lib._on_server_attrs_hook, hook.handle) | ||
| hook.hexchat_hook = handle | ||
| return id(hook) | ||
|
|
||
|
|
||
| def hook_timer(timeout, callback, userdata=None): | ||
| plugin = __get_current_plugin() | ||
| hook = plugin.add_hook(callback, userdata) | ||
| handle = lib.hexchat_hook_timer(lib.ph, timeout, lib._on_timer_hook, hook.handle) | ||
| hook.hexchat_hook = handle | ||
| return id(hook) | ||
|
|
||
|
|
||
| def hook_unload(callback, userdata=None): | ||
| plugin = __get_current_plugin() | ||
| hook = plugin.add_hook(callback, userdata, is_unload=True) | ||
| return id(hook) | ||
|
|
||
|
|
||
| def unhook(handle): | ||
| plugin = __get_current_plugin() | ||
| return plugin.remove_hook(handle) | ||
|
|
||
|
|
||
| def set_pluginpref(name, value): | ||
| if isinstance(value, str): | ||
| return bool(lib.hexchat_pluginpref_set_str(lib.ph, name.encode(), value.encode())) | ||
|
|
||
| if isinstance(value, int): | ||
| return bool(lib.hexchat_pluginpref_set_int(lib.ph, name.encode(), value)) | ||
|
|
||
| # XXX: This should probably raise but this keeps API | ||
| return False | ||
|
|
||
|
|
||
| def get_pluginpref(name): | ||
| name = name.encode() | ||
| string_out = ffi.new('char[512]') | ||
| if lib.hexchat_pluginpref_get_str(lib.ph, name, string_out) != 1: | ||
| return None | ||
|
|
||
| string = ffi.string(string_out) | ||
| # This API stores everything as a string so we have to figure out what | ||
| # its actual type was supposed to be. | ||
| if len(string) > 12: # Can't be a number | ||
| return __decode(string) | ||
|
|
||
| number = lib.hexchat_pluginpref_get_int(lib.ph, name) | ||
| if number == -1 and string != b'-1': | ||
| return __decode(string) | ||
|
|
||
| return number | ||
|
|
||
|
|
||
| def del_pluginpref(name): | ||
| return bool(lib.hexchat_pluginpref_delete(lib.ph, name.encode())) | ||
|
|
||
|
|
||
| def list_pluginpref(): | ||
| prefs_str = ffi.new('char[4096]') | ||
| if lib.hexchat_pluginpref_list(lib.ph, prefs_str) == 1: | ||
| return __decode(ffi.string(prefs_str)).split(',') | ||
|
|
||
| return [] | ||
|
|
||
|
|
||
| class Context: | ||
| def __init__(self, ctx): | ||
| self._ctx = ctx | ||
|
|
||
| def __eq__(self, value): | ||
| if not isinstance(value, Context): | ||
| return False | ||
|
|
||
| return self._ctx == value._ctx | ||
|
|
||
| @contextmanager | ||
| def __change_context(self): | ||
| old_ctx = lib.hexchat_get_context(lib.ph) | ||
| if not self.set(): | ||
| # XXX: Behavior change, previously used wrong context | ||
| lib.hexchat_print(lib.ph, b'Context object refers to closed context, ignoring call') | ||
| return | ||
|
|
||
| yield | ||
| lib.hexchat_set_context(lib.ph, old_ctx) | ||
|
|
||
| def set(self): | ||
| # XXX: API addition, C plugin silently ignored failure | ||
| return bool(lib.hexchat_set_context(lib.ph, self._ctx)) | ||
|
|
||
| def prnt(self, string): | ||
| with self.__change_context(): | ||
| prnt(string) | ||
|
|
||
| def emit_print(self, event_name, *args, **kwargs): | ||
| time = kwargs.pop('time', 0) # For py2 compat | ||
| with self.__change_context(): | ||
| return emit_print(event_name, *args, time=time) | ||
|
|
||
| def command(self, string): | ||
| with self.__change_context(): | ||
| command(string) | ||
|
|
||
| def get_info(self, name): | ||
| with self.__change_context(): | ||
| return get_info(name) | ||
|
|
||
| def get_list(self, name): | ||
| with self.__change_context(): | ||
| return get_list(name) | ||
|
|
||
|
|
||
| def get_context(): | ||
| ctx = lib.hexchat_get_context(lib.ph) | ||
| return Context(ctx) | ||
|
|
||
|
|
||
| def find_context(server=None, channel=None): | ||
| server = server.encode() if server is not None else ffi.NULL | ||
| channel = channel.encode() if channel is not None else ffi.NULL | ||
| ctx = lib.hexchat_find_context(lib.ph, server, channel) | ||
| if ctx == ffi.NULL: | ||
| return None | ||
|
|
||
| return Context(ctx) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,89 @@ | ||
| #!/usr/bin/env python3 | ||
|
|
||
| import sys | ||
| import cffi | ||
|
|
||
| builder = cffi.FFI() | ||
|
|
||
| # hexchat-plugin.h | ||
| with open(sys.argv[1]) as f: | ||
| output = [] | ||
| eat_until_endif = 0 | ||
| # This is very specific to hexchat-plugin.h, it is not a cpp | ||
| for line in f: | ||
| if line.startswith('#define'): | ||
| continue | ||
| elif line.endswith('HEXCHAT_PLUGIN_H\n'): | ||
| continue | ||
| elif 'time.h' in line: | ||
| output.append('typedef int... time_t;') | ||
| elif line.startswith('#if'): | ||
| eat_until_endif += 1 | ||
| elif line.startswith('#endif'): | ||
| eat_until_endif -= 1 | ||
| elif eat_until_endif and '_hexchat_context' not in line: | ||
| continue | ||
| else: | ||
| output.append(line) | ||
| builder.cdef(''.join(output)) | ||
|
|
||
| builder.embedding_api(''' | ||
| extern "Python" int _on_py_command(char **, char **, void *); | ||
| extern "Python" int _on_load_command(char **, char **, void *); | ||
| extern "Python" int _on_unload_command(char **, char **, void *); | ||
| extern "Python" int _on_reload_command(char **, char **, void *); | ||
| extern "Python" int _on_say_command(char **, char **, void *); | ||
| extern "Python" int _on_command_hook(char **, char **, void *); | ||
| extern "Python" int _on_print_hook(char **, void *); | ||
| extern "Python" int _on_print_attrs_hook(char **, hexchat_event_attrs *, void *); | ||
| extern "Python" int _on_server_hook(char **, char **, void *); | ||
| extern "Python" int _on_server_attrs_hook(char **, char **, hexchat_event_attrs *, void *); | ||
| extern "Python" int _on_timer_hook(void *); | ||
| extern "Python" int _on_plugin_init(char **, char **, char **, char *, char *); | ||
| extern "Python" int _on_plugin_deinit(void); | ||
| static hexchat_plugin *ph; | ||
| ''') | ||
|
|
||
| builder.set_source('_hexchat_embedded', ''' | ||
| /* Python's header defines these.. */ | ||
| #undef HAVE_MEMRCHR | ||
| #undef HAVE_STRINGS_H | ||
| #include "config.h" | ||
| #include "hexchat-plugin.h" | ||
| static hexchat_plugin *ph; | ||
| CFFI_DLLEXPORT int _on_plugin_init(char **, char **, char **, char *, char *); | ||
| CFFI_DLLEXPORT int _on_plugin_deinit(void); | ||
| int hexchat_plugin_init(hexchat_plugin *plugin_handle, | ||
| char **name_out, char **description_out, | ||
| char **version_out, char *arg) | ||
| { | ||
| if (ph != NULL) | ||
| { | ||
| puts ("Python plugin already loaded\\n"); | ||
| return 0; /* Prevent loading twice */ | ||
| } | ||
| ph = plugin_handle; | ||
| return _on_plugin_init(name_out, description_out, version_out, arg, HEXCHATLIBDIR); | ||
| } | ||
| int hexchat_plugin_deinit(void) | ||
| { | ||
| int ret = _on_plugin_deinit(); | ||
| ph = NULL; | ||
| return ret; | ||
| } | ||
| ''') | ||
|
|
||
| # python.py | ||
| with open(sys.argv[2]) as f: | ||
| builder.embedding_init_code(f.read()) | ||
|
|
||
| # python.c | ||
| builder.emit_c_code(sys.argv[3]) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| from _hexchat import * |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,3 @@ | ||
| EXPORTS | ||
| hexchat_plugin_init | ||
| hexchat_plugin_deinit | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| # HexChat Python Module Style Guide | ||
|
|
||
| (This is a work in progress). | ||
|
|
||
| ## General rules | ||
|
|
||
| - PEP8 as general fallback recommendations | ||
| - Max line length: 120 | ||
| - Avoid overcomplex compound statements. i.e. dont do this: `somevar = x if x == y else z if a == b and c == b else x` | ||
|
|
||
| ## Indentation style | ||
|
|
||
| ### Multi-line functions | ||
|
|
||
| ```python | ||
| foo(really_long_arg_1, | ||
| really_long_arg_2) | ||
| ``` | ||
|
|
||
| ### Mutli-line lists/dicts | ||
|
|
||
| ```python | ||
| foo = { | ||
| 'bar': 'baz', | ||
| } | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| from _hexchat import * |