From 09d4c520431aa955e507f950ac00c9f56d07163b Mon Sep 17 00:00:00 2001 From: Emile Clark-Boman Date: Tue, 24 Jun 2025 16:42:17 +1000 Subject: [PATCH] init --- .gitignore | 1 + README | 1 + imp/__init__.py | 0 imp/constants.py | 19 ++++ imp/crypto/__init__.py | 0 imp/crypto/cyphers.py | 32 ++++++ imp/math/__init__.py | 0 imp/math/groups.py | 37 +++++++ imp/math/primes.py | 67 ++++++++++++ imp/math/util.py | 2 + imp/structs/__init__.py | 1 + imp/structs/__result.py | 19 ++++ sandbox/__init__.py | 0 sandbox/cli/__init__.py | 0 sandbox/cli/prompt.py | 66 ++++++++++++ sandbox/cli/style.py | 234 ++++++++++++++++++++++++++++++++++++++++ sandbox/cmd/__init__.py | 0 sandbox/cmd/cycsub.py | 60 +++++++++++ sandbox/exceptions.py | 3 + 19 files changed, 542 insertions(+) create mode 100644 .gitignore create mode 100644 README create mode 100644 imp/__init__.py create mode 100644 imp/constants.py create mode 100644 imp/crypto/__init__.py create mode 100644 imp/crypto/cyphers.py create mode 100644 imp/math/__init__.py create mode 100644 imp/math/groups.py create mode 100644 imp/math/primes.py create mode 100644 imp/math/util.py create mode 100644 imp/structs/__init__.py create mode 100644 imp/structs/__result.py create mode 100644 sandbox/__init__.py create mode 100644 sandbox/cli/__init__.py create mode 100644 sandbox/cli/prompt.py create mode 100644 sandbox/cli/style.py create mode 100644 sandbox/cmd/__init__.py create mode 100644 sandbox/cmd/cycsub.py create mode 100644 sandbox/exceptions.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c18dd8d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +__pycache__/ diff --git a/README b/README new file mode 100644 index 0000000..f616f3f --- /dev/null +++ b/README @@ -0,0 +1 @@ +The "imbaud python library" (imp lib), or just imp for short! diff --git a/imp/__init__.py b/imp/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/imp/constants.py b/imp/constants.py new file mode 100644 index 0000000..73d70aa --- /dev/null +++ b/imp/constants.py @@ -0,0 +1,19 @@ +''' +ASCII Character Ranges +''' +# Common +WHITESPACE = '\t\n\r\x0b\x0c' +DIGITS = '0123456789' +ALPHA_LOWER = 'abcdefghijklmnopqrstuvwxyz' +ALPHA_UPPER = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' +ALPHA = ALPHA_LOWER + ALPHA_UPPER +ALPHANUM = ALPHA + DIGITS +SYMBOLS = '!\"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~' +# Other +DIGITS_BIN = '01' +DIGITS_OCT = '01234567' +DIGITS_HEX_LOWER = '0123456789abcdef' +DIGITS_HEX_UPPER = '0123456789ABCDEF' +CHARSET_HEX = '0123456789abcdefABCDEF' +DIGITS_B64 = ALPHANUM + '+/' +CHARSET_B64 = DIGITS_B64 + '=' # Base64 charset contains = padding diff --git a/imp/crypto/__init__.py b/imp/crypto/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/imp/crypto/cyphers.py b/imp/crypto/cyphers.py new file mode 100644 index 0000000..b4b1297 --- /dev/null +++ b/imp/crypto/cyphers.py @@ -0,0 +1,32 @@ +from imp.constants import ALPHA_LOWER, ALPHA_UPPER + +ERRMSG_ROT_CHARSET_DUPL = 'Bad charsets for ROT: found duplicate character \'{0}\'' +ERRMSG_ROT_CHAR_UNKNOWN = 'No charset for ROT has character \'{0}\' (exception thrown since `ignore_noncharset=False`)' + +''' +Substitution Cyphers +''' +def ROT(plaintext: str, + n: int, + charsets: list[str] = [ALPHA_LOWER, ALPHA_UPPER], + ignore_noncharset: bool = True) -> str: + cyphertext = '' + for c in plaintext: + index = None + for charset in charsets: + try: + i = charset.index(c) + # if we reached here then no ValueError occured + if index is not None: + raise ValueError(ERRMSG_ROT_CHARSET_DUPL.format(c)) + break + except ValueError: pass + if index is not None: + cyphertext += charset[(i + n) % len(charset)] + elif not ignore_noncharset: + raise ValueError(ERRMSG_ROT_CHAR_UNKNOWN.format(c)) + return cyphertext + +# Common ROT aliases +def ROT13(plaintext: str) -> str: return ROT(plaintext, 13) +def caesar(plaintext: str) -> str: return ROT(plaintext, 1) diff --git a/imp/math/__init__.py b/imp/math/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/imp/math/groups.py b/imp/math/groups.py new file mode 100644 index 0000000..25028ef --- /dev/null +++ b/imp/math/groups.py @@ -0,0 +1,37 @@ +''' +This library exists to isolate all math functions +related to groups and their representations. +''' + +from math import gcd + +''' +Returns the multiplicative cyclic subgroup +generated by an element g modulo m. +Returns the cyclic subgroup as a list[int], + the order of that subgroup, and a boolean + indicating whether g is infinitely repeating + with period == ord (or otherwise if it + terminates with g**ord == 0). +''' +def cyclic_subgrp(g: int, + m: int, + ignore_zero: bool = True) -> tuple[list[int], int, bool]: + G = [] + order = 0 + periodic = True + a = 1 # start at identity + for _ in range(m): + a = (a * g) % m + if a == 0: + if not ignore_zero: + G.append(a) + order += 1 + periodic = False + break + # check if we've reached something periodic + elif a in G[:1]: + break + G.append(a) + order += 1 + return G, order, periodic diff --git a/imp/math/primes.py b/imp/math/primes.py new file mode 100644 index 0000000..513f61e --- /dev/null +++ b/imp/math/primes.py @@ -0,0 +1,67 @@ +from math import gcd + +''' +Euler's Totient (Phi) Function +''' +def totient(n: int) -> int: + phi = int(n > 1 and n) + for p in range(2, int(n ** .5) + 1): + if not n % p: + phi -= phi // p + while not n % p: + n //= p + #if n is > 1 it means it is prime + if n > 1: phi -= phi // n + return phi + +''' +Tests the primality of an integer using its totient. +NOTE: If totient(n) has already been calculated + then pass it as the optional phi parameter. +''' +def is_prime(n: int, phi: int = None) -> bool: + return n - 1 == (phi if phi is not None else totient(n)) + +''' +Prime number generator function. +Returns the tuple (p, phi(p)) where p is prime + and phi is Euler's totient function. +''' +def prime_gen(yield_phi: bool = False) -> int | tuple[int, int]: + n = 1 + while True: + n += 1 + phi = totient(n) + if is_prime(n, phi=phi): + if yield_phi: + yield (n, phi) + else: + yield n + +''' +Returns the prime factorisation of a number. +Returns a list of tuples (p, m) where p is +a prime factor and m is its multiplicity. +NOTE: uses a trial division algorithm +''' +def prime_factors(n: int) -> list[tuple[int, int]]: + phi = totient(n) + if is_prime(n, phi=phi): + return [(n, 1)] + factors = [] + for p in prime_gen(yield_phi=False): + if p >= n: + break + # check if divisor + multiplicity = 0 + while n % p == 0: + n //= p + multiplicity += 1 + if multiplicity: + factors.append((p, multiplicity)) + if is_prime(n): + break + if n != 1: + factors.append((n, 1)) + return factors + diff --git a/imp/math/util.py b/imp/math/util.py new file mode 100644 index 0000000..e6c160d --- /dev/null +++ b/imp/math/util.py @@ -0,0 +1,2 @@ +def digits(n: int) -> int: + return len(str(n)) diff --git a/imp/structs/__init__.py b/imp/structs/__init__.py new file mode 100644 index 0000000..2e02aaf --- /dev/null +++ b/imp/structs/__init__.py @@ -0,0 +1 @@ +from noether.lib.structs.__result import Result diff --git a/imp/structs/__result.py b/imp/structs/__result.py new file mode 100644 index 0000000..8746de0 --- /dev/null +++ b/imp/structs/__result.py @@ -0,0 +1,19 @@ +from enum import Enum +from typing import Optional + +class Result: + def __init__(self, + success: bool, + reason: str, + value: Optional[any] = None) -> None: + self.success = success + self.reason = reason + self.value = value + + @classmethod + def succeed(cls, value: any, reason: str = 'Ok') -> 'Result': + return cls(True, reason, value=value) + + @classmethod + def fail(cls, reason: str, value: Optional[any] = None) -> 'Result': + return cls(False, reason, value=value) diff --git a/sandbox/__init__.py b/sandbox/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sandbox/cli/__init__.py b/sandbox/cli/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sandbox/cli/prompt.py b/sandbox/cli/prompt.py new file mode 100644 index 0000000..0a6d05c --- /dev/null +++ b/sandbox/cli/prompt.py @@ -0,0 +1,66 @@ +''' +===== Prompt Handling ===== +This file implements the Prompt class, allowing for +inheritance and instantiation of Prompt objects. +The CLI class uses these instead of containing the +logic immediately within itself. +''' + +from typing import Any + +from noether.cli.style import * +from noether.lib.structs import Result + +class Prompt: + DEFAULT_PROMPT = 'DEF> ' + def __init__(self) -> None: + self._prompt = self.DEFAULT_PROMPT + + ''' + Prompt and await command via stdin. + ''' + def prompt(self): + command = self.__request() + result = self._parse(command) + if result.success: + self._exec(result.value) + else: + self.__parse_error(result) + + ''' + !! OVERRIDE ON INHERITANCE !! + Handles the parsing of a given command. + ''' + def _parse(self, command: str) -> Result: + return Result.succeed(command) + + def __parse_error(self, error: Result) -> None: + err = f' ↳ {style(error.reason, Effect.ITALICS)}' + print(style(err, Effect.DIM)) + + ''' + !! OVERRIDE ON INHERITANCE !! + Handles the execution of a command that + was successfully parsed by a Prompt inheritor. + ''' + def _exec(self, command: Any) -> None: + pass + + ''' + Internal use only. Handles a raw request with no validation. + ''' + def __request(self) -> None: + print(self.__get_prompt(), end='', flush=True) + return input() + + ''' + !! OVERRIDE ON INHERITANCE !! + ''' + def _style_prompt(self, prompt: str) -> str: + return prompt + + def __get_prompt(self) -> str: + return self._style_prompt(self._prompt) + + def set_prompt(self, prompt: str) -> None: + self._prompt = prompt diff --git a/sandbox/cli/style.py b/sandbox/cli/style.py new file mode 100644 index 0000000..eda6fd9 --- /dev/null +++ b/sandbox/cli/style.py @@ -0,0 +1,234 @@ +''' +=== ANSI Escape Sequences === +This file exists to organise and name +all important ANSI escape sequences +for management of the CLI. +''' + +from sys import stdout +from enum import StrEnum + +# Removes ALL set colors/styling +RESET = '\x1b[0m' + +''' +Implements text foreground coloring. +''' +class Color(StrEnum): + BLACK = '\x1b[30m' + RED = '\x1b[31m' + GREEN = '\x1b[32m' + YELLOW = '\x1b[33m' + BLUE = '\x1b[34m' + MAGENTA = '\x1b[35m' + CYAN = '\x1b[36m' + WHITE = '\x1b[37m' + + ''' + Handles the application of a Color to a given text. + Unset the `temp` flag to leave this style on (after the given text) + ''' + @staticmethod + def apply(color: 'Color', text: str, temp: bool = True) -> str: + return color + text + Color.WHITE if temp else '' + +''' +Implements text effects. +''' +class Effect(StrEnum): + BOLD = '\x1b[1m' + DIM = '\x1b[2m' + ITALICS = '\x1b[3m' + UNDERLINE = '\x1b[4m' + BLINK = '\x1b[5m' + REVERSE = '\x1b[7m' + HIDE = '\x1b[8m' + + ''' + Returns the opposite ESC code to a given Effect. + ie the opposite of BOLD '\x1b[1m]' is '\x1b[21m' + ''' + @staticmethod + def _inverse(effect: 'Effect') -> str: + return f'{effect[:2]}2{effect[2:]}' + + ''' + Handles the application of a Effect to a given text. + Unset the `temp` flag to leave this style on (after the given text) + ''' + @staticmethod + def apply(effect: 'Effect', text: str, temp: bool = True) -> str: + return effect + text + Effect._inverse(effect) if temp else '' + +''' +Applies styling (color/effects/etc) to a given string +Unset the `temp` flag to leave this style on (after the given text) +''' +def style(text: str, *args: Color | Effect, temp: bool = True) -> str: + # unsures we don't add redundant "set white" commands + unset_color = False + for arg in args: + unset = temp + if isinstance(arg, Color): + unset_color |= temp + unset = False + text = type(arg).apply(arg, text, temp=True) + if unset_color: + text += Color.WHITE + return text + + +''' +Implements cursor movement functionality. +NOTE: + The Cursor class currently has no ability + to handle EXACT line (row) numbers. Currently + I'm assuming all functionality of noether can + be implemented solely via relative movements. +''' +class Cursor(StrEnum): + # SAVE current / RESTORE last saved cursor position + _SAVE = f'\x1b[7' + _RESTORE = f'\x1b[8' + + _MV_UP = 'A' + _MV_DOWN = 'B' + _MV_LEFT = 'C' + _MV_RIGHT = 'D' + # NEXT/PREV are the same as DOWN/UP (respectively) + # except that the cursor will reset to column 0 + _MV_NEXT = 'E' + _MV_PREV = 'F' + # move cursor to the start of the current line + _MV_START = '\r' # there is no ESC CSI for carriage return + + _MV_COLUMN = 'G' + + ''' + Generates an ESC code sequence corresponding + to a horizontal movement relative to the + cursor's current vertical position. + +ive = right + -ive = left + ''' + @staticmethod + def _XMOVE_REL(n: int) -> str: + if n > 0: + return f'\x1b[{n}{Cursor._MV_RIGHT}' + if n < 0: + return f'\x1b[{n}{Cursor._MV_LEFT}' + return '' + + ''' + Generates an ESC code sequence corresponding + to a horizontal movement to an EXACT column. + ''' + @staticmethod + def _XMOVE(n: int) -> str: + return f'\x1b[{n}{Cursor._MV_COLUMN}' + + ''' + Generates an ESC code sequence corresponding + to a vertical movement relative to the + cursor's current vertical position. + +ive = down + -ive = up + ''' + @staticmethod + def _YMOVE_REL(n: int, reset: bool = False) -> str: + if n > 0: + if reset: + return f'\x1b[{n}{Cursor._MV_NEXT}' + return f'\x1b[{n}{Cursor._MV_DOWN}' + if n < 0: + if reset: + return f'\x1b[{n}{Cursor._MV_PREV}' + return f'\x1b[{n}{Cursor._MV_UP}' + return '' + + ''' + Sets the cursor column (horizontal) position to an exact value. + NOTE: does NOT flush stdout buffer + ''' + @staticmethod + def set_x(n: int) -> None: + stdout.write(Cursor.XMOVE(n)) + + ''' + Moves the cursor left/right n columns (relative). + NOTE: does NOT flush stdout buffer + ''' + @staticmethod + def move_x(n: int) -> None: + stdout.write(Cursor._XMOVE_REL(n)) + + ''' + Moves the cursor up/down n rows and resets the + cursor to be at the start of the line + NOTE: does NOT flush stdout buffer + ''' + @staticmethod + def move_y(n: int, reset: bool = True) -> None: + stdout.write(Cursor._YMOVE_REL(n, reset=reset)) + + ''' + Saves the current cursor position. + NOTE: does NOT flush stdout buffer + ''' + @staticmethod + def save() -> None: + stdout.write(Cursor._SAVE) + + ''' + Restores the cursor position to a saved position. + NOTE: does NOT flush stdout buffer + ''' + @staticmethod + def restore() -> None: + stdout.write(Cursor._RESTORE) + +''' +Handles erasing content displayed on the screen. +NOTE that the cursor position is NOT updated +via these sequences. The \r code should be given after. +''' +class Erase(StrEnum): + # erase everything from the current cursor + # position to the START/END of the screen + _SCR_AFTER = '\x1b[0J' + _SCR_BEFORE = '\x1b[1J' + _SCR_ALL = '\x1b[3J' # erase screen and delete all saved cursors + + # erase everything from the current cursor + # position to the START/END of the current line + _LINE_AFTER = '\x1b[0K' + _LINE_BEFORE = '\x1b[1K' + _LINE_ALL = '\x1b[2K' + + ''' + Erase characters on the entire screen. + Set `before` flag to only erase before the cursor, + set `after` flag to only erase after the cursor. + ''' + @staticmethod + def screen(before: bool = False, after: bool = False) -> None: + if before: + stdout.write(Erase._SCR_BEFORE) + elif after: + stdout.write(Erase._SCR_AFTER) + else: + stdout.write(Erase._SCR_ALL) + + ''' + Erase characters on the current line. + Set `before` flag to only erase before the cursor, + set `after` flag to only erase after the cursor. + ''' + @staticmethod + def line(before: bool = False, after: bool = False) -> None: + if before: + stdout.write(Erase._LINE_BEFORE) + elif after: + stdout.write(Erase._LINE_AFTER) + else: + stdout.write(Erase._LINE_ALL) diff --git a/sandbox/cmd/__init__.py b/sandbox/cmd/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sandbox/cmd/cycsub.py b/sandbox/cmd/cycsub.py new file mode 100644 index 0000000..afb3ca3 --- /dev/null +++ b/sandbox/cmd/cycsub.py @@ -0,0 +1,60 @@ +from sys import stdout + +from noether.cli.style import * +from noether.cli.prompt import * +from noether.lib.structs import Result + +from noether.lib.util import digits +from noether.lib.groups import cyclic_subgrp +from noether.lib.primes import totient, is_prime + +class cycsub(Prompt): + DEFAULT_PROMPT = style('[n]: ', Color.BLUE) + def __init__(self, ignore_zero: bool = True) -> None: + super().__init__() + self.ignore_zero = ignore_zero + + def _parse(self, command: str) -> int: + try: + return Result.succeed(int(command)) + except ValueError: + return Result.fail('Not an integer.') + + def _exec(self, n: int) -> None: + phi = totient(n) + lpadding = digits(n) + rpadding = digits(phi) + if is_prime(n, phi=n): + Cursor.save() + Cursor.move_y(-1, reset=False) + Cursor.set_x(len(self.DEFAULT_PROMPT) + lpadding + 1) + stdout.write(style('[PRIME]', Color.MAGENTA, Effect.BOLD)) + Cursor.restore() + stdout.flush() + + # keeps track of all primitive roots + # (note that there will be exactly totient(phi) of them) + proots = [] + for g in range(n): + G, order, periodic = cyclic_subgrp(g, n, ignore_zero=self.ignore_zero) + primitive = (order == phi) # primitive root + + lpad = ' ' * (lpadding - digits(a)) + rpad = ' ' * (rpadding - digits(order)) + + color_g = Color.RED + style_G = [] + style_order = [] + if primitive: + color_g = Color.GREEN + style_G = [Color.GREEN, Effect.BOLD] + style_order = [Color.YELLOW, Effect.BOLD] + proots.append(g) + elif gcd(g, n) == 1: + color_g = Color.Yellow + line = style(f'{lpad}{g}', color_g, Effect.BOLD) + \ + '-> ' + \ + style(f'{order}{rpad} ', *style_order) + \ + '| ' + \ + style(str(G), *style_G) + print(line, flush=True) diff --git a/sandbox/exceptions.py b/sandbox/exceptions.py new file mode 100644 index 0000000..75c314e --- /dev/null +++ b/sandbox/exceptions.py @@ -0,0 +1,3 @@ +class PromptParseError(Exception): + def __init__(self, message: str) -> None: + super().__init__(message)