Comportamiento y resultados diferentes al canalizar en CMD y PowerShell

Resuelto Adel M. asked hace 4 años • 1 respuestas

Estoy intentando canalizar el contenido de un archivo a un programa de cifrado simétrico ASCII simple que hice. Es un programa simple que lee la entrada de STDIN y suma o resta un cierto valor (224) a cada byte de la entrada. Por ejemplo: si el primer byte es 4 y queremos cifrar, entonces se convierte en 228. Si excede 255, el programa simplemente realiza algún módulo.

Este es el resultado que obtengo con cmd (test.txt contiene "esto es una prueba"):

    type .\test.txt | .\Crypt.exe --encrypt | .\Crypt.exe --decrypt
    this is a test

También funciona al revés, por lo que es un algoritmo de cifrado simétrico.

    type .\test.txt | .\Crypt.exe --decrypt | .\Crypt.exe --encrypt
    this is a test

Pero el comportamiento en PowerShell es diferente. Al cifrar primero, obtengo:

    type .\test.txt | .\Crypt.exe --encrypt | .\Crypt.exe --decrypt
    this is a test_*

Y eso es lo que obtengo al descifrar primero:

Captura de pantalla

Quizás sea un problema de codificación. Gracias de antemano.

Adel M. avatar Nov 30 '19 03:11 Adel M.
Aceptado

tl;dr :

  • En Windows PowerShell y PowerShell (Core) hasta v7.3.x , si necesita manejo de bytes sin formato y/o necesita evitar que PowerShell agregue una nueva línea final a sus datos de texto, evite por completo la canalización de PowerShell , como se muestra a continuación.

  • Las soluciones alternativas ya no son necesarias en v7.4+ : en v7.4, la característica previamente experimentalPSNativeCommandPreserveBytePipe denominada se convirtió en una característica estable : >y |cuando se aplica a programas externos (nativos) ahora actúan como conductos de bytes sin procesar , es decir, omiten la decodificación de cadenas habitual. y ciclo de recodificación a favor de pasar los datos sin procesar.

    • Sin embargo, persisten dos limitaciones :
      • El envío de una cadena de PowerShell a través de la canalización a un programa externo siempre provoca que se agregue una nueva línea . Consulte esta respuesta para encontrar soluciones.
      • No puede capturar los bytes sin procesar que forman la salida de un programa externo en la memoria en PowerShell; Consulte la sección inferior para encontrar soluciones.

Para el manejo de bytes sin formato en Windows PowerShell y PowerShell v7.3- , pague con cmd(/c en Windows; en plataformas tipo Unix/subsistemas Windows tipo Unix, use sho bashcon-c ):

cmd /c 'type .\test.txt | .\Crypt.exe --encrypt | .\Crypt.exe --decrypt'

Utilice una técnica similar para guardar la salida de bytes sin procesar en un archivo ; no utilice el operador de PowerShell> :

cmd /c 'someexe > file.bin'

Tenga en cuenta que si desea capturar la salida de texto de un programa externo en una variable de PowerShell o procesarla aún más en una canalización de PowerShell , debe asegurarse de que [Console]::OutputEncodingcoincida con la codificación de caracteres de salida de su programa (la página de códigos OEM activa, generalmente), que debería ser verdadero por defecto en este caso; consulte la siguiente sección para obtener más detalles.

Sin embargo, en general es mejor evitar la manipulación de bytes de datos de texto .


Hay dos problemas separados , de los cuales sólo uno tiene una solución sencilla:


Problema 1 : De hecho, existe un problema de codificación de caracteres , como sospechaba:

PowerShell se inserta de forma invisible como intermediario en las canalizaciones, incluso cuando envía y recibe datos de programas externos : convierte datos desde y hacia cadenas .NET ( System.String), que son secuencias de unidades de código UTF-16.

  • Como comentario aparte: incluso cuando se usan solo comandos nativos de PowerShell, esto significa que leer la entrada de los archivos y guardarlos nuevamente puede dar como resultado una codificación de caracteres diferente, porque la información sobre la codificación de caracteres original no se conserva una vez que se han almacenado los datos (cadena). se lee en la memoria y, al guardarlo, se utiliza la codificación de caracteres predeterminada de los cmdlets; Si bien esta codificación predeterminada es consistentemente UTF-8 sin BOM en PowerShell (Core) 6+ , varía según el cmdlet en Windows PowerShell ; consulte esta respuesta .

Para enviar y recibir datos de programas externos (como Crypt.exeen su caso), debe hacer coincidir su codificación de caracteres ; en su caso, con una aplicación de consola de Windows que utiliza el manejo de bytes sin formato , la codificación implícita es la página de códigos OEM activa del sistema.

  • Al enviar datos , PowerShell utiliza la codificación de la $OutputEncodingvariable de preferencia para codificar (lo que invariablemente se trata como texto), que de forma predeterminada es ASCII(!) en Windows PowerShell y (sin BOM) UTF-8 en PowerShell (Core).

  • El extremo receptor está cubierto de forma predeterminada: PowerShell utiliza [Console]::OutputEncoding(que a su vez refleja la página de códigos informada por chcp) para decodificar los datos recibidos, y en Windows esto refleja de forma predeterminada la página de códigos OEM activa, tanto en Windows PowerShell como en PowerShell [Core] [1] .

Para solucionar su problema principal, debe configurar $OutputEncodingla página de códigos OEM activa :

# Make sure that PowerShell uses the OEM code page when sending
# data to `.\Crypt.exe`
$OutputEncoding = [Console]::OutputEncoding

Problema 2 : PowerShell invariablemente agrega una nueva línea final a los datos que aún no tienen una cuando canaliza datos a programas externos:

Es decir, "foo" | .\Crypt.exeno envía (los $OutputEncodingbytes codificados que representan) "foo"a .\Crypt.exela entrada estándar de 's, envía "foo`r`n"en Windows; es decir, una secuencia de nueva línea (apropiada para la plataforma) (CRLF en Windows) se agrega automática e invariablemente (a menos que la cadena ya tenga una nueva línea al final).

Este comportamiento problemático se analiza en el número 5974 de GitHub y también en esta respuesta .

En su caso específico, el agregado implícito "`r`n"también está sujeto al cambio de valor de byte, lo que significa que la primera Crypt.exellamada lo transforma en -*, lo que provoca que se agregue otro "`r`n" cuando los datos se envían a la segunda Crypt.exellamada.

El resultado neto es una nueva línea adicional de ida y vuelta (la intermedia -*), más una nueva línea cifrada que da como resultado φΩ).


En resumen: si sus datos de entrada no tenían una nueva línea final, tendrá que cortar los últimos 4 caracteres del resultado (que representan las secuencias de nueva línea de ida y vuelta y las secuencias de nueva línea cifradas inadvertidamente):

# Ensure that .\Crypt.exe output is correctly decoded.
$OutputEncoding = [Console]::OutputEncoding

# Invoke the command and capture its output in variable $result.
# Note the use of the `Get-Content` cmdlet; in PowerShell, `type`
# is simply a built-in *alias* for it.
$result = Get-Content .\test.txt | .\Crypt.exe --decrypt | .\Crypt.exe --encrypt

# Remove the last 4 chars. and print the result.
$result.Substring(0, $result.Length - 4)

Dado que llamar cmd /ccomo se muestra en la parte superior de la respuesta también funciona, no parece que valga la pena.


Cómo maneja PowerShell los datos de canalización con programas externos:

Nota : Lo siguiente también se aplica principalmente a v7.4+ , excepto donde se indique lo contrario. (PowerShell) v7.3: es una abreviatura de las versiones anteriores de PowerShell (Core) (7.3.x e inferiores) y de Windows PowerShell .

A diferencia cmd(o shells tipo POSIX como bash):

  • PowerShell v7.3: no admite datos de bytes sin procesar en canalizaciones . [2]

  • Cuando habla con programas externos , solo reconoce texto (mientras que pasa objetos .NET cuando habla con los propios comandos de PowerShell, que es de donde proviene gran parte de su poder).

En concreto, esto funciona de la siguiente manera:

  • Cuando envía datos a un programa externo a través de la canalización (a su flujo estándar):

    • En v7.4+ , ahora puede enviar datos de bytes sin procesar a un programa externo, como un flujo de [byte]instancias o, preferiblemente, para un mejor rendimiento, como una [byte[]]matriz ; un caso de uso para esta técnica es evitar que PowerShell agregue una nueva línea final al texto, lo que invariablemente ocurre cuando se envía una cadena (consulte esta respuesta para obtener detalles y una solución alternativa 7.3); p.ej

      # Sends bytes with values 65 and 66 to findstr.exe, 
      # which interprets them as single-byte characters 'A' and 'B'
      # The unary form of "," in effect sends the byte array
      # *as a whole*, which improves performance.
      , [byte[]] (65, 66) | findstr . # -> 'AB'
      
    • De lo contrario, e invariablemente en v7.3, se convierte a texto (cadenas) usando la codificación de caracteres especificada en la $OutputEncodingvariable de preferencia , que por defecto es ASCII(!) en Windows PowerShell y (sin BOM) UTF-8 en PowerShell. (Centro) .

      • Advertencia : si asigna una codificación con una lista de materiales a $OutputEncoding, PowerShell emitirá la lista de materiales como parte de la primera línea de salida enviada a un programa externo; por lo tanto, por ejemplo, no use [System.Text.Encoding]::UTF8(que emite una lista de materiales) en Windows PowerShell y use [System.Text.Utf8Encoding]::new()(que no lo hace) en su lugar.

      • Si PowerShell no captura ni redirige los datos, es posible que los problemas de codificación no siempre sean evidentes, es decir, si se implementa un programa externo de manera que utilice la API de la consola Unicode de Windows para imprimir en la pantalla.

    • Algo que aún no es texto (una cadena) se codifica usando el formato de salida predeterminado de PowerShell (el mismo formato que ves cuando imprimes en la consola), con una advertencia importante :

      • Si el (último) objeto de entrada ya es una cadena que no tiene una nueva línea final , invariablemente se agrega una (e incluso una nueva línea final existente se reemplaza por la nativa de la plataforma, si es diferente).

        • Este comportamiento puede causar problemas, como se analiza en el número 5974 de GitHub y también en esta respuesta .
  • Cuando captura/redirecciona datos de un programa externo (desde su flujo de salida estándar), invariablemente se decodifica como líneas de texto (cadenas), según la codificación especificada en [Console]::OutputEncoding, que de forma predeterminada es la página de códigos OEM activa en Windows (sorprendentemente, en ambas ediciones de PowerShell, a partir de v7.0-preview6 [1] ).

  • Internamente en PowerShell, el texto se representa utilizando el System.Stringtipo .NET , que se basa en unidades de código UTF-16 (a menudo llamado de manera vaga, pero incorrecta, "Unicode" [3] ).

En Windows PowerShell y PowerShell (Core) hasta v7.3.x únicamente , lo anterior también se aplica :

  • al canalizar datos entre programas externos ,

  • cuando los datos se redirigen a un archivo ; es decir, independientemente del origen de los datos y su codificación de caracteres original, PowerShell utiliza su (s) codificación(es) predeterminada(s) al enviar datos a archivos; en Windows PowerShell , >produce archivos codificados en UTF-16LE (con BOM), mientras que PowerShell (Core) utiliza de forma predeterminada UTF-8 sin BOM (consistentemente, en todos los cmdlets de escritura de archivos).

En v7.4+ , PowerShell ahora transmite bytes sin procesar en los dos escenarios anteriores , lo que no solo mejora notablemente el rendimiento, sino que también evita una posible corrupción de datos debido a la interpretación anterior como texto.


Tenga en cuenta que no es posible capturar datos de bytes sin procesar de programas externos en la memoria : al asignar una variable o al procesarla mediante un comando de PowerShell , la interpretación como texto todavía se aplica invariablemente; la solución más sencilla es:

  • v7.4+ : Úselo >para redirigir la llamada del programa externo a un archivo y leer ese archivo con [System.IO.File::ReadAllBytes(), por ejemplo; Como siempre, al pasar rutas del sistema de archivos a métodos .NET, asegúrese de pasar rutas completas , porque el directorio de trabajo de .NET generalmente difiere del de PowerShell.

  • v7.3- : llame al programa externo a través del shell nativo de la plataforma y use su > operador para capturar la salida de bytes sin procesar en un archivo ( cmd /c 'foo.exe ... > fileen Windows, sh -c 'foo ... > file'en plataformas tipo Unix), luego lea el archivo en PowerShell, como se indica arriba.


[1] En PowerShell (Core), dado que es $OutputEncodingdigno de elogio que ya esté predeterminado en UTF-8, tendría sentido que fuera [Console]::OutputEncodinglo mismo, es decir, que la página de códigos activa esté efectivamente 65001en Windows, como se sugiere en el número 7233 de GitHub .

[2] Con la entrada de un archivo , lo más cerca que puede estar del manejo de bytes sin formato es leer el archivo como una matriz .NETSystem.Byte con Get-Content -AsByteStream(PowerShell (Core))/ Get-Content -Encoding Byte(Windows PowerShell), pero la única manera de procesarlos aún más como matriz es canalizar a un comando de PowerShell que está diseñado para manejar una matriz de bytes, o pasándolo a un método de tipo .NET que espera una matriz de bytes. Si intentara enviar dicha matriz a un programa externo a través de la canalización, cada byte se enviaría como su representación de cadena decimal en su propia línea .

[3] Unicode es el nombre del estándar abstracto que describe un "alfabeto global". En su uso concreto, cuenta con diversas codificaciones estándar , siendo UTF-8 y UTF-16 las más utilizadas.

mklement0 avatar Nov 30 '2019 17:11 mklement0