Ctrl K
ComunidadGuías…

app/services/text_encoding.rb

Rama develop · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# Normalizes content HELIOS reads from disk (or from a child process) to plain
# UTF-8: decodes UTF-16, repairs Latin-1/CP1252 bytes and drops a leading byte
# order mark.
#
# Users edit `.env` with whatever editor their NAS or desktop ships, and many
# of those still default to Latin-1/CP1252. A single umlaut in a comment is
# then enough to make every String operation on the file raise
# (Encoding::CompatibilityError / ArgumentError: invalid byte sequence in
# UTF-8), which took down the whole start page. Normalizing at the read
# boundary keeps the rest of the code free of encoding checks; writing the
# file back repairs it permanently.
#
# compose.yaml deliberately gets no such treatment: docker itself refuses to
# parse non-UTF-8 YAML, so that stack cannot work either way.
module TextEncoding
  module_function

  # The marks a UTF-16 file starts with (little and big endian). Only the mark
  # reveals that a file is not single-byte at all; its remaining bytes look
  # like ordinary Latin-1 garbage to the repair below, which would turn the
  # whole file into mojibake and then parse zero variables out of it.
  UTF_16_BOMS = ["\xFF\xFE".b, "\xFE\xFF".b].freeze

  def utf8(content)
    bytes = content.to_s.b
    string = utf16(bytes) || bytes.force_encoding(Encoding::UTF_8)

    # The mark itself is valid UTF-8, but poison for anything that parses the
    # first line: editors offer "UTF-8 with BOM" (Synology's does) and docker
    # compose drops it silently, so HELIOS has to as well. Dropped before the
    # repair below, which would turn the mark into "".
    string = string.delete_prefix("\uFEFF")
    return string if string.valid_encoding?

    repair(string)
  end

  # Repairs the invalid byte sequences one at a time and leaves everything that
  # is already valid untouched. Hand-edited files are regularly mixed, e.g. a
  # Latin-1 umlaut in a comment next to a UTF-8 umlaut in a password: with the
  # whole file transcoded at once, "Grün" would become "Grün" and #save would
  # persist that, leaving the services with a password their data volume does
  # not know.
  def repair(string)
    string.scrub { |invalid| transcode(invalid, Encoding::WINDOWS_1252) || transcode(invalid, Encoding::ISO_8859_1) }
  end

  # CP1252 first (superset of Latin-1 in the printable range, so it also
  # recovers curly quotes and dashes from Windows editors), then Latin-1,
  # which maps all 256 byte values and therefore always succeeds.
  def transcode(string, from)
    string.encode(Encoding::UTF_8, from)
  rescue EncodingError
    nil
  end

  # Ruby's UTF-16 reads the endianness off the mark and consumes it. Replacing
  # what a truncated file cuts in half costs a single character, while falling
  # through to the single-byte repair would cost the whole file. The two
  # :replace options make the conversion total, so it cannot raise.
  def utf16(bytes)
    return unless UTF_16_BOMS.include?(bytes.byteslice(0, 2))

    bytes.encode(Encoding::UTF_8, Encoding::UTF_16, invalid: :replace, undef: :replace)
  end
end