""" Java java.nio.charset APIs implemented in Python. java.nio.charset package - Charset, CharsetEncoder, CharsetDecoder, CoderResult, etc. Wraps Python codecs module. """ from __future__ import annotations from .._core import Stub, AndroidOnlyError import codecs as _codecs import encodings as _encodings from typing import Set as _Set, List as _List __all__ = [ "Charset", "CharsetEncoder", "CharsetDecoder", "CoderResult", "CodingErrorAction", "StandardCharsets", ] # ============================================================ # CodingErrorAction # ============================================================ class CodingErrorAction(Stub): """ java.nio.charset.CodingErrorAction Describes actions that a charset encoder or decoder may take on a coding error. """ REPORT: "CodingErrorAction" = None IGNORE: "CodingErrorAction" = None REPLACE: "CodingErrorAction" = None def __init__(self, name: str): super().__init__() self._name = name def name(self) -> str: return self._name def toString(self) -> str: return self._name def equals(self, other) -> bool: if isinstance(other, CodingErrorAction): return self._name == other._name return False def hashCode(self) -> int: return hash(self._name) & 0x7FFFFFFF CodingErrorAction.REPORT = CodingErrorAction("REPORT") CodingErrorAction.IGNORE = CodingErrorAction("IGNORE") CodingErrorAction.REPLACE = CodingErrorAction("REPLACE") # ============================================================ # CoderResult # ============================================================ class CoderResult(Stub): """ java.nio.charset.CoderResult Describes the result state of a coder (encoder or decoder). """ UNDERFLOW: "CoderResult" = None OVERFLOW: "CoderResult" = None def __init__(self, _type: str, _length: int = 0): super().__init__() self._type = _type self._length = _length def isUnderflow(self) -> bool: """Returns True if the result is underflow (more input needed).""" return self._type == "UNDERFLOW" def isOverflow(self) -> bool: """Returns True if the result is overflow (output buffer too small).""" return self._type == "OVERFLOW" def isError(self) -> bool: """Returns True if the result is an error (malformed or unmappable).""" return self._type in ("MALFORMED", "UNMAPPABLE") def isMalformed(self) -> bool: """Returns True if the result is a malformed-input error.""" return self._type == "MALFORMED" def isUnmappable(self) -> bool: """Returns True if the result is an unmappable-character error.""" return self._type == "UNMAPPABLE" def length(self) -> int: """Returns the length of the erroneous input (for errors).""" return self._length @classmethod def malformedForLength(cls, length: int) -> "CoderResult": """Creates a malformed CoderResult for the given length.""" return cls("MALFORMED", length) @classmethod def unmappableForLength(cls, length: int) -> "CoderResult": """Creates an unmappable CoderResult for the given length.""" return cls("UNMAPPABLE", length) def toString(self) -> str: if self._type == "UNDERFLOW": return "UNDERFLOW" elif self._type == "OVERFLOW": return "OVERFLOW" return f"{self._type}[{self._length}]" def equals(self, other) -> bool: if not isinstance(other, CoderResult): return False return self._type == other._type and self._length == other._length def hashCode(self) -> int: return hash((self._type, self._length)) & 0x7FFFFFFF CoderResult.UNDERFLOW = CoderResult("UNDERFLOW") CoderResult.OVERFLOW = CoderResult("OVERFLOW") # ============================================================ # Charset # ============================================================ class Charset(Stub): """ java.nio.charset.Charset A named mapping between sequences of 16-bit Unicode characters and sequences of bytes. """ _cache: dict = {} _aliases: dict = {} # Standard charset constants US_ASCII: "Charset" = None ISO_8859_1: "Charset" = None UTF_8: "Charset" = None UTF_16: "Charset" = None UTF_16BE: "Charset" = None UTF_16LE: "Charset" = None def __init__(self, canonical_name: str, aliases: _List[str] = None): super().__init__() self._name = canonical_name self._aliases = aliases or [] self._registered = True def name(self) -> str: """Returns the canonical name of this charset.""" return self._name def displayName(self, locale=None) -> str: """Returns the display name of this charset.""" return self._name def aliases(self) -> _Set[str]: """Returns a set containing this charset's aliases.""" return set(self._aliases) def isRegistered(self) -> bool: """Returns True if this charset is registered in the IANA registry.""" return self._registered def canEncode(self) -> bool: """Returns True if this charset supports encoding.""" try: _codecs.lookup(self._name) return True except LookupError: return False def encode(self, text: str) -> "ByteBuffer": """ Converts a string into bytes using this charset. Returns a ByteBuffer. """ encoded = text.encode(self._name, errors="replace") from ..nio import ByteBuffer buf = ByteBuffer.allocate(len(encoded)) buf.put(list(encoded)) buf.flip() return buf def decode(self, buf: "ByteBuffer") -> "CharBuffer": """ Converts bytes from a ByteBuffer into a CharBuffer. """ arr = bytearray() for i in range(buf.position(), buf.limit()): arr.append(buf.get(i)) decoded = arr.decode(self._name, errors="replace") from ..nio import CharBuffer cb = CharBuffer.allocate(len(decoded)) cb.put(decoded) cb.flip() return cb def newDecoder(self) -> "CharsetDecoder": """Returns a new CharsetDecoder for this charset.""" return CharsetDecoder(self) def newEncoder(self) -> "CharsetEncoder": """Returns a new CharsetEncoder for this charset.""" return CharsetEncoder(self) def toString(self) -> str: return self._name def hashCode(self) -> int: return hash(self._name) & 0x7FFFFFFF def equals(self, other) -> bool: if not isinstance(other, Charset): return False return self._name == other._name def compareTo(self, other: "Charset") -> int: """Compares this charset to another by canonical name.""" if not isinstance(other, Charset): return -1 if self._name < other._name: return -1 elif self._name > other._name: return 1 return 0 @classmethod def forName(cls, charset_name: str) -> "Charset": """Returns a Charset for the given charset name.""" if charset_name in cls._cache: return cls._cache[charset_name] try: _codecs.lookup(charset_name) except LookupError: raise AndroidOnlyError(f"Charset not found: {charset_name}") # Resolve aliases canonical = charset_name.lower() alias_map = { "ascii": "US-ASCII", "us-ascii": "US-ASCII", "iso-8859-1": "ISO-8859-1", "latin1": "ISO-8859-1", "utf-8": "UTF-8", "utf8": "UTF-8", "utf-16": "UTF-16", "utf16": "UTF-16", "utf-16be": "UTF-16BE", "utf16be": "UTF-16BE", "utf-16le": "UTF-16LE", "utf16le": "UTF-16LE", } canonical_name = alias_map.get(canonical, charset_name.upper()) charset = cls(canonical_name) cls._cache[charset_name] = charset return charset @classmethod def forNameUEE(cls, charset_name: str) -> "Charset": """Same as forName but wraps exception.""" try: return cls.forName(charset_name) except Exception: return None @classmethod def isSupported(cls, charset_name: str) -> bool: """Returns True if the named charset is supported.""" try: _codecs.lookup(charset_name) return True except LookupError: return False @classmethod def defaultCharset(cls) -> "Charset": """Returns the default charset of the JVM (platform native encoding).""" import locale enc = locale.getpreferredencoding() return cls.forName(enc) @classmethod def availableCharsets(cls) -> dict: """Returns a sorted map of available charset names to Charset objects.""" result = {} for name in _codecs.available_encodings(): try: result[name] = cls.forName(name) except Exception: pass return result @classmethod def requiredCharsets(cls) -> _Set[str]: """Returns the set of required charset names per Java spec.""" return { "US-ASCII", "ISO-8859-1", "UTF-8", "UTF-16", "UTF-16BE", "UTF-16LE", } # Initialize standard charset constants Charset.US_ASCII = Charset("US-ASCII", ["ASCII", "US-ASCII", "ascii", "cp367"]) Charset.ISO_8859_1 = Charset("ISO-8859-1", ["ISO-8859-1", "latin1", "LATIN-1", "l1", "cp819"]) Charset.UTF_8 = Charset("UTF-8", ["UTF-8", "utf8", "UTF8"]) Charset.UTF_16 = Charset("UTF-16", ["UTF-16", "utf16", "UTF16", "UnicodeBig"]) Charset.UTF_16BE = Charset("UTF-16BE", ["UTF-16BE", "utf16be", "UTF16BE", "ISO-10646-UCS-2"]) Charset.UTF_16LE = Charset("UTF-16LE", ["UTF-16LE", "utf16le", "UTF16LE"]) # ============================================================ # CharsetEncoder # ============================================================ class CharsetEncoder(Stub): """ java.nio.charset.CharsetEncoder Converts character data to byte data using a Charset. """ def __init__(self, charset: Charset, average_bytes_per_char: float = 1.0, max_bytes_per_char: float = 4.0, replacement: bytes = b'?'): super().__init__() self._charset = charset self._average_bytes_per_char = average_bytes_per_char self._max_bytes_per_char = max_bytes_per_char self._replacement = replacement self._malformed_input_action = CodingErrorAction.REPORT self._unmappable_action = CodingErrorAction.REPORT def charset(self) -> Charset: """Returns the charset that created this encoder.""" return self._charset def replacement(self) -> bytes: """Returns the replacement value.""" return self._replacement def replaceWith(self, new_replacement: bytes) -> "CharsetEncoder": """Sets a new replacement value.""" self._replacement = new_replacement return self def encode(self, input_str: str) -> "ByteBuffer": """Encodes a string into bytes.""" encoded = input_str.encode(self._charset.name(), errors="replace") from ..nio import ByteBuffer buf = ByteBuffer.allocate(len(encoded)) buf.put(list(encoded)) buf.flip() return buf def encode(self, input: "CharBuffer", output: "ByteBuffer", end_of_input: bool) -> CoderResult: """Encodes as much as possible from input to output.""" in_str = "" for i in range(input.position(), input.limit()): in_str += chr(input.get(i)) try: encoded = in_str.encode(self._charset.name()) from ..nio import ByteBuffer out_arr = bytearray() for i in range(output.position(), output.limit()): if i < len(encoded): out_arr.append(encoded[i]) return CoderResult.UNDERFLOW except UnicodeEncodeError: return CoderResult.unmappableForLength(1) def encodeLoop(self, input: "CharBuffer", output: "ByteBuffer") -> CoderResult: """The encoding loop (subclass hook).""" return self.encode(input, output, True) def implReplaceWith(self, new_replacement: bytes): """Hook for subclasses to react to replacement changes.""" pass def implOnMalformedInput(self, new_action: CodingErrorAction): """Hook for subclasses to react to malformed input action changes.""" pass def implOnUnmappableCharacter(self, new_action: CodingErrorAction): """Hook for subclasses to react to unmappable char action changes.""" pass def implReset(self): """Hook for subclasses to reset internal state.""" pass def averageBytesPerChar(self) -> float: return self._average_bytes_per_char def maxBytesPerChar(self) -> float: return self._max_bytes_per_char def isLegalReplacement(self, repl: bytes) -> bool: """Returns True if the replacement is a legal byte sequence.""" return len(repl) > 0 def malformedInputAction(self) -> CodingErrorAction: return self._malformed_input_action def unmappableCharacterAction(self) -> CodingErrorAction: return self._unmappable_action def onMalformedInput(self, action: CodingErrorAction) -> "CharsetEncoder": self._malformed_input_action = action return self def onUnmappableCharacter(self, action: CodingErrorAction) -> "CharsetEncoder": self._unmappable_action = action return self def reset(self) -> "CharsetEncoder": """Resets this encoder, clearing any internal state.""" self.implReset() return self def toString(self) -> str: return f"CharsetEncoder@{hex(id(self))}[charset={self._charset.name()}]" def equals(self, other) -> bool: if not isinstance(other, CharsetEncoder): return False return self._charset == other._charset def hashCode(self) -> int: return hash(self._charset.name()) & 0x7FFFFFFF # ============================================================ # CharsetDecoder # ============================================================ class CharsetDecoder(Stub): """ java.nio.charset.CharsetDecoder Converts byte data to character data using a Charset. """ def __init__(self, charset: Charset, average_chars_per_byte: float = 1.0, max_chars_per_byte: float = 2.0, replacement: str = '\ufffd'): super().__init__() self._charset = charset self._average_chars_per_byte = average_chars_per_byte self._max_chars_per_byte = max_chars_per_byte self._replacement = replacement self._malformed_input_action = CodingErrorAction.REPORT self._unmappable_action = CodingErrorAction.REPORT self._auto_detect = False self._detected_charset = None def charset(self) -> Charset: """Returns the charset that created this decoder.""" return self._charset def replacement(self) -> str: """Returns the replacement value.""" return self._replacement def replaceWith(self, new_replacement: str) -> "CharsetDecoder": """Sets a new replacement value.""" self._replacement = new_replacement return self def decode(self, buf: "ByteBuffer") -> "CharBuffer": """Decodes bytes from a ByteBuffer into a CharBuffer.""" arr = bytearray() for i in range(buf.position(), buf.limit()): arr.append(buf.get(i)) decoded = arr.decode(self._charset.name(), errors="replace") from ..nio import CharBuffer cb = CharBuffer.allocate(len(decoded)) cb.put(decoded) cb.flip() return cb def decode(self, input: "ByteBuffer", output: "CharBuffer", end_of_input: bool) -> CoderResult: """Decodes as much as possible from input to output.""" arr = bytearray() for i in range(input.position(), input.limit()): arr.append(input.get(i)) try: decoded = arr.decode(self._charset.name()) return CoderResult.UNDERFLOW except UnicodeDecodeError: return CoderResult.malformedForLength(1) def decodeLoop(self, input: "ByteBuffer", output: "CharBuffer") -> CoderResult: """The decoding loop (subclass hook).""" return self.decode(input, output, True) def implReplaceWith(self, new_replacement: str): """Hook for subclasses.""" pass def implOnMalformedInput(self, new_action: CodingErrorAction): """Hook for subclasses.""" pass def implOnUnmappableCharacter(self, new_action: CodingErrorAction): """Hook for subclasses.""" pass def implReset(self): """Hook for subclasses.""" pass def averageCharsPerByte(self) -> float: return self._average_chars_per_byte def maxCharsPerByte(self) -> float: return self._max_chars_per_byte def isCharsetDetected(self) -> bool: return self._detected_charset is not None def isAutoDetect(self) -> bool: return self._auto_detect def detectedCharset(self) -> Charset: if self._detected_charset is None: raise AndroidOnlyError("No charset detected") return self._detected_charset def malformedInputAction(self) -> CodingErrorAction: return self._malformed_input_action def unmappableCharacterAction(self) -> CodingErrorAction: return self._unmappable_action def onMalformedInput(self, action: CodingErrorAction) -> "CharsetDecoder": self._malformed_input_action = action return self def onUnmappableCharacter(self, action: CodingErrorAction) -> "CharsetDecoder": self._unmappable_action = action return self def reset(self) -> "CharsetDecoder": self.implReset() return self def toString(self) -> str: return f"CharsetDecoder@{hex(id(self))}[charset={self._charset.name()}]" def equals(self, other) -> bool: if not isinstance(other, CharsetDecoder): return False return self._charset == other._charset def hashCode(self) -> int: return hash(self._charset.name()) & 0x7FFFFFFF # ============================================================ # StandardCharsets # ============================================================ class StandardCharsets(Stub): """ java.nio.charset.StandardCharsets Constant definitions for the standard charsets. """ US_ASCII: Charset = Charset.US_ASCII ISO_8859_1: Charset = Charset.ISO_8859_1 UTF_8: Charset = Charset.UTF_8 UTF_16: Charset = Charset.UTF_16 UTF_16BE: Charset = Charset.UTF_16BE UTF_16LE: Charset = Charset.UTF_16LE