From a5e3c739fc41c3f5c7369a89714b9ccc6dfd86cd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Feb 2026 09:33:52 +0000 Subject: [PATCH 01/13] Initial plan From 08d07791370b40c89727ed61e94ca26a09aac63e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Feb 2026 09:36:26 +0000 Subject: [PATCH 02/13] Fix Windows compatibility and logic error in gammu_SaveRingtone Co-authored-by: nijel <212189+nijel@users.noreply.github.com> --- gammu/src/gammu.c | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/gammu/src/gammu.c b/gammu/src/gammu.c index 9ec91c775..b18c801aa 100644 --- a/gammu/src/gammu.c +++ b/gammu/src/gammu.c @@ -31,8 +31,13 @@ #include /* File creation and permissions */ +#ifdef _WIN32 +#include +#include +#else #include #include +#endif /* Strings */ #ifdef HAVE_STRING_H @@ -6074,7 +6079,7 @@ gammu_SaveRingtone(PyObject *self, PyObject *args, PyObject *kwds) return NULL; } - fd = PyObject_AsFileDescriptor(value); + fd = PyObject_AsFileDescriptor(file); if (fd == -1) { PyErr_Clear(); } @@ -6085,10 +6090,10 @@ gammu_SaveRingtone(PyObject *self, PyObject *args, PyObject *kwds) f = fdopen(fd, "wb"); if (f == NULL) return NULL; closefile = TRUE; - } else if (PyUnicode_Check(value)) { + } else if (PyUnicode_Check(file)) { int ofd; - str = PyUnicode_EncodeFSDefault(value); + str = PyUnicode_EncodeFSDefault(file); if (str == NULL) { return NULL; } @@ -6097,7 +6102,13 @@ gammu_SaveRingtone(PyObject *self, PyObject *args, PyObject *kwds) Py_DECREF(str); return NULL; } +#ifdef _WIN32 + /* Windows: use _open with _S_IREAD | _S_IWRITE for owner read/write */ + ofd = _open(name, _O_WRONLY | _O_CREAT | _O_TRUNC | _O_BINARY, _S_IREAD | _S_IWRITE); +#else + /* POSIX: use open with S_IRUSR | S_IWUSR for owner read/write */ ofd = open(name, O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR); +#endif Py_DECREF(str); if (ofd == -1) { PyErr_SetString(PyExc_IOError, "Can not open file for writing!"); @@ -6105,7 +6116,11 @@ gammu_SaveRingtone(PyObject *self, PyObject *args, PyObject *kwds) } f = fdopen(ofd, "wb"); if (f == NULL) { +#ifdef _WIN32 + _close(ofd); +#else close(ofd); +#endif PyErr_SetString(PyExc_IOError, "Can not open file for writing!"); return NULL; } From b5d49768d1a85f58d1df4f15bf35e6d0f06440a9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Feb 2026 09:37:15 +0000 Subject: [PATCH 03/13] Refactor headers to avoid duplication of sys/stat.h Co-authored-by: nijel <212189+nijel@users.noreply.github.com> --- gammu/src/gammu.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/gammu/src/gammu.c b/gammu/src/gammu.c index b18c801aa..496c7335f 100644 --- a/gammu/src/gammu.c +++ b/gammu/src/gammu.c @@ -31,12 +31,11 @@ #include /* File creation and permissions */ +#include #ifdef _WIN32 #include -#include #else #include -#include #endif /* Strings */ From 8a41e906c7d6b0dd1a5cfc45ddf1ae0b56a4b941 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Feb 2026 09:43:59 +0000 Subject: [PATCH 04/13] Add test for SaveRingtone file permissions and fix typos pre-commit issue Co-authored-by: nijel <212189+nijel@users.noreply.github.com> --- pyproject.toml | 3 +++ test/test_dummy.py | 45 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 2a8fd43f0..1eff2c0c7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -185,6 +185,9 @@ sort_inline_tables = true sort_table_keys = true spaces_before_inline_comment = 2 +[tool.typos.default] +extend-words = {WRONLY = "WRONLY"} + [tool.typos.files] extend-exclude = [ "test/data/*.vcf", diff --git a/test/test_dummy.py b/test/test_dummy.py index 8bbfb3a65..eb2064861 100644 --- a/test/test_dummy.py +++ b/test/test_dummy.py @@ -383,6 +383,51 @@ def test_getnextfile(self) -> None: assert folders == 3 assert files == 6 + def test_save_ringtone_permissions(self) -> None: + """Test that SaveRingtone creates files with restrictive permissions.""" + import stat + + # Create a simple ringtone dictionary + ringtone = { + "Name": "Test", + "Notes": [ + {"Note": "C", "Duration": 4, "Scale": 1} + ] + } + + # Create a temporary file path + with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.rttl') as f: + temp_file = f.name + + try: + # Remove the file so SaveRingtone can create it + os.unlink(temp_file) + + # Save the ringtone + gammu.SaveRingtone(temp_file, ringtone, "rttl") + + # Check that the file was created + assert os.path.exists(temp_file) + + # Check file permissions - should be owner read/write only + # Skip permission check on Windows as it handles permissions differently + if platform.system() != "Windows": + file_stat = os.stat(temp_file) + file_mode = stat.S_IMODE(file_stat.st_mode) + + # File should have owner read/write permissions (0o600) + # Check that group and others don't have any permissions + assert (file_mode & stat.S_IRWXG) == 0, f"Group has permissions: {oct(file_mode)}" + assert (file_mode & stat.S_IRWXO) == 0, f"Others have permissions: {oct(file_mode)}" + + # Verify owner has read and write permissions + assert (file_mode & stat.S_IRUSR) != 0, f"Owner missing read permission: {oct(file_mode)}" + assert (file_mode & stat.S_IWUSR) != 0, f"Owner missing write permission: {oct(file_mode)}" + finally: + # Clean up + if os.path.exists(temp_file): + os.unlink(temp_file) + def test_incoming_call(self) -> None: self.check_incoming_call() self._called = False From 52427ba9e09a45f76de9b504c51ffc8662048ecf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Feb 2026 09:45:14 +0000 Subject: [PATCH 05/13] Improve test code quality per code review feedback Co-authored-by: nijel <212189+nijel@users.noreply.github.com> --- test/test_dummy.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/test/test_dummy.py b/test/test_dummy.py index eb2064861..1c1fa6b6a 100644 --- a/test/test_dummy.py +++ b/test/test_dummy.py @@ -24,6 +24,7 @@ import pathlib import platform import shutil +import stat import tempfile import unittest @@ -385,8 +386,6 @@ def test_getnextfile(self) -> None: def test_save_ringtone_permissions(self) -> None: """Test that SaveRingtone creates files with restrictive permissions.""" - import stat - # Create a simple ringtone dictionary ringtone = { "Name": "Test", @@ -395,14 +394,11 @@ def test_save_ringtone_permissions(self) -> None: ] } - # Create a temporary file path - with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.rttl') as f: - temp_file = f.name + # Create a unique temporary file path + temp_dir = tempfile.gettempdir() + temp_file = os.path.join(temp_dir, f"test_ringtone_{os.getpid()}.rttl") try: - # Remove the file so SaveRingtone can create it - os.unlink(temp_file) - # Save the ringtone gammu.SaveRingtone(temp_file, ringtone, "rttl") From 2daacd881cafeaa45f5cd21db0d2f038c931f8ae Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Feb 2026 09:46:09 +0000 Subject: [PATCH 06/13] Further improve test to handle race conditions and use secure temp file creation Co-authored-by: nijel <212189+nijel@users.noreply.github.com> --- test/test_dummy.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/test/test_dummy.py b/test/test_dummy.py index 1c1fa6b6a..28291f922 100644 --- a/test/test_dummy.py +++ b/test/test_dummy.py @@ -394,9 +394,11 @@ def test_save_ringtone_permissions(self) -> None: ] } - # Create a unique temporary file path - temp_dir = tempfile.gettempdir() - temp_file = os.path.join(temp_dir, f"test_ringtone_{os.getpid()}.rttl") + # Create a unique temporary file path using a secure method + # We need to close and delete it so SaveRingtone can create it + with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.rttl') as f: + temp_file = f.name + os.unlink(temp_file) # Remove it so SaveRingtone can create it fresh try: # Save the ringtone @@ -420,9 +422,11 @@ def test_save_ringtone_permissions(self) -> None: assert (file_mode & stat.S_IRUSR) != 0, f"Owner missing read permission: {oct(file_mode)}" assert (file_mode & stat.S_IWUSR) != 0, f"Owner missing write permission: {oct(file_mode)}" finally: - # Clean up - if os.path.exists(temp_file): + # Clean up - use try-except to handle case where file doesn't exist + try: os.unlink(temp_file) + except FileNotFoundError: + pass def test_incoming_call(self) -> None: self.check_incoming_call() From 1ef64c8a30eba115cefd817ef2822eca5d4e489f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Thu, 5 Feb 2026 11:03:09 +0100 Subject: [PATCH 07/13] Apply suggestion from @nijel --- test/test_dummy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_dummy.py b/test/test_dummy.py index 28291f922..2017dcb91 100644 --- a/test/test_dummy.py +++ b/test/test_dummy.py @@ -396,7 +396,7 @@ def test_save_ringtone_permissions(self) -> None: # Create a unique temporary file path using a secure method # We need to close and delete it so SaveRingtone can create it - with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.rttl') as f: + with tempfile.NamedTemporaryFile(mode='wb', delete=False, suffix='.rttl') as f: temp_file = f.name os.unlink(temp_file) # Remove it so SaveRingtone can create it fresh From cbf772b4eb9964addb0caecf19b6ac2804dbd2f9 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 5 Feb 2026 10:04:44 +0000 Subject: [PATCH 08/13] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- test/test_dummy.py | 42 ++++++++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/test/test_dummy.py b/test/test_dummy.py index 2017dcb91..372c2f817 100644 --- a/test/test_dummy.py +++ b/test/test_dummy.py @@ -19,6 +19,7 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # +import contextlib import datetime import os.path import pathlib @@ -387,46 +388,47 @@ def test_getnextfile(self) -> None: def test_save_ringtone_permissions(self) -> None: """Test that SaveRingtone creates files with restrictive permissions.""" # Create a simple ringtone dictionary - ringtone = { - "Name": "Test", - "Notes": [ - {"Note": "C", "Duration": 4, "Scale": 1} - ] - } - + ringtone = {"Name": "Test", "Notes": [{"Note": "C", "Duration": 4, "Scale": 1}]} + # Create a unique temporary file path using a secure method # We need to close and delete it so SaveRingtone can create it - with tempfile.NamedTemporaryFile(mode='wb', delete=False, suffix='.rttl') as f: + with tempfile.NamedTemporaryFile(mode="wb", delete=False, suffix=".rttl") as f: temp_file = f.name os.unlink(temp_file) # Remove it so SaveRingtone can create it fresh - + try: # Save the ringtone gammu.SaveRingtone(temp_file, ringtone, "rttl") - + # Check that the file was created assert os.path.exists(temp_file) - + # Check file permissions - should be owner read/write only # Skip permission check on Windows as it handles permissions differently if platform.system() != "Windows": file_stat = os.stat(temp_file) file_mode = stat.S_IMODE(file_stat.st_mode) - + # File should have owner read/write permissions (0o600) # Check that group and others don't have any permissions - assert (file_mode & stat.S_IRWXG) == 0, f"Group has permissions: {oct(file_mode)}" - assert (file_mode & stat.S_IRWXO) == 0, f"Others have permissions: {oct(file_mode)}" - + assert (file_mode & stat.S_IRWXG) == 0, ( + f"Group has permissions: {oct(file_mode)}" + ) + assert (file_mode & stat.S_IRWXO) == 0, ( + f"Others have permissions: {oct(file_mode)}" + ) + # Verify owner has read and write permissions - assert (file_mode & stat.S_IRUSR) != 0, f"Owner missing read permission: {oct(file_mode)}" - assert (file_mode & stat.S_IWUSR) != 0, f"Owner missing write permission: {oct(file_mode)}" + assert (file_mode & stat.S_IRUSR) != 0, ( + f"Owner missing read permission: {oct(file_mode)}" + ) + assert (file_mode & stat.S_IWUSR) != 0, ( + f"Owner missing write permission: {oct(file_mode)}" + ) finally: # Clean up - use try-except to handle case where file doesn't exist - try: + with contextlib.suppress(FileNotFoundError): os.unlink(temp_file) - except FileNotFoundError: - pass def test_incoming_call(self) -> None: self.check_incoming_call() From 083db8d9fc62ab8cf85446300298a405e7bfdc2f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Feb 2026 10:09:32 +0000 Subject: [PATCH 09/13] Fix Windows compilation by including fcntl.h for both platforms Co-authored-by: nijel <212189+nijel@users.noreply.github.com> --- gammu/src/gammu.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/gammu/src/gammu.c b/gammu/src/gammu.c index 496c7335f..6e853429f 100644 --- a/gammu/src/gammu.c +++ b/gammu/src/gammu.c @@ -32,10 +32,9 @@ /* File creation and permissions */ #include +#include #ifdef _WIN32 #include -#else -#include #endif /* Strings */ From 46bdc0dd8c87793fdbf0ce233440af76d400a2a3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Feb 2026 10:40:19 +0000 Subject: [PATCH 10/13] Changes before error encountered Co-authored-by: nijel <212189+nijel@users.noreply.github.com> --- test/test_dummy.py | 53 +++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 48 insertions(+), 5 deletions(-) diff --git a/test/test_dummy.py b/test/test_dummy.py index 372c2f817..5872bc8bc 100644 --- a/test/test_dummy.py +++ b/test/test_dummy.py @@ -19,6 +19,7 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # +import contextlib import contextlib import datetime import os.path @@ -387,12 +388,27 @@ def test_getnextfile(self) -> None: def test_save_ringtone_permissions(self) -> None: """Test that SaveRingtone creates files with restrictive permissions.""" - # Create a simple ringtone dictionary - ringtone = {"Name": "Test", "Notes": [{"Note": "C", "Duration": 4, "Scale": 1}]} + # Create a complete ringtone dictionary with all required fields + ringtone = { + "Name": "TestRingtone", + "Notes": [ + { + "Type": "Note", + "Value": 113, # Note value + "Tempo": 120, + "Scale": 1, + "Style": "Natural", + "Note": "C", + "Duration": "1/4", + "DurationSpec": "NoSpecialDuration", + } + ], + } - # Create a unique temporary file path using a secure method - # We need to close and delete it so SaveRingtone can create it - with tempfile.NamedTemporaryFile(mode="wb", delete=False, suffix=".rttl") as f: + # Test 1: Save using string filename + with tempfile.NamedTemporaryFile( + mode="wb", delete=False, suffix=".rttl" + ) as f: temp_file = f.name os.unlink(temp_file) # Remove it so SaveRingtone can create it fresh @@ -430,6 +446,33 @@ def test_save_ringtone_permissions(self) -> None: with contextlib.suppress(FileNotFoundError): os.unlink(temp_file) + # Test 2: Test multiple formats to ensure comprehensive coverage + formats_to_test = ["rttl", "ott", "imy"] + for fmt in formats_to_test: + with tempfile.NamedTemporaryFile( + mode="wb", delete=False, suffix=f".{fmt}" + ) as f: + temp_file = f.name + os.unlink(temp_file) + + try: + gammu.SaveRingtone(temp_file, ringtone, fmt) + assert os.path.exists(temp_file), f"File not created for format {fmt}" + + # Verify permissions for each format + if platform.system() != "Windows": + file_stat = os.stat(temp_file) + file_mode = stat.S_IMODE(file_stat.st_mode) + assert (file_mode & stat.S_IRWXG) == 0, ( + f"Format {fmt}: Group has permissions" + ) + assert (file_mode & stat.S_IRWXO) == 0, ( + f"Format {fmt}: Others have permissions" + ) + finally: + with contextlib.suppress(FileNotFoundError): + os.unlink(temp_file) + def test_incoming_call(self) -> None: self.check_incoming_call() self._called = False From 1ffa03c506435d8079dbd20a9fc7ec3cb95d7253 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 5 Feb 2026 10:46:31 +0000 Subject: [PATCH 11/13] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- test/test_dummy.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/test/test_dummy.py b/test/test_dummy.py index 5872bc8bc..07ab79d85 100644 --- a/test/test_dummy.py +++ b/test/test_dummy.py @@ -19,7 +19,6 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # -import contextlib import contextlib import datetime import os.path @@ -406,9 +405,7 @@ def test_save_ringtone_permissions(self) -> None: } # Test 1: Save using string filename - with tempfile.NamedTemporaryFile( - mode="wb", delete=False, suffix=".rttl" - ) as f: + with tempfile.NamedTemporaryFile(mode="wb", delete=False, suffix=".rttl") as f: temp_file = f.name os.unlink(temp_file) # Remove it so SaveRingtone can create it fresh From 5575449273e3fb0716de32db88269007b5343345 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Feb 2026 12:43:17 +0000 Subject: [PATCH 12/13] Fix ringtone test data: use valid Scale value (220 instead of 1) Co-authored-by: nijel <212189+nijel@users.noreply.github.com> --- test/test_dummy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_dummy.py b/test/test_dummy.py index 07ab79d85..dfd6bc2e4 100644 --- a/test/test_dummy.py +++ b/test/test_dummy.py @@ -395,7 +395,7 @@ def test_save_ringtone_permissions(self) -> None: "Type": "Note", "Value": 113, # Note value "Tempo": 120, - "Scale": 1, + "Scale": 220, # Valid scale: 55, 110, 220, 440, or 880 "Style": "Natural", "Note": "C", "Duration": "1/4", From d77bd6b12a1d586251c21b448a0cd0ab0108f81f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Thu, 5 Feb 2026 13:51:13 +0100 Subject: [PATCH 13/13] Apply suggestion from @nijel --- test/test_dummy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_dummy.py b/test/test_dummy.py index dfd6bc2e4..eb85234e0 100644 --- a/test/test_dummy.py +++ b/test/test_dummy.py @@ -398,7 +398,7 @@ def test_save_ringtone_permissions(self) -> None: "Scale": 220, # Valid scale: 55, 110, 220, 440, or 880 "Style": "Natural", "Note": "C", - "Duration": "1/4", + "Duration": "1_4", "DurationSpec": "NoSpecialDuration", } ],