Watch at EaseHistorias. Respuestas. Perspectiva.

Tecnología · GUÍA PRÁCTICA

¿El ZIP de GitHub solo trae punteros LFS? Verifica el contenido antes de descartar el clon

Una descarga correcta puede contener solo punteros. Comprueba los bytes descomprimidos de un miembro exacto con el puntero de un commit de confianza y distingue esa coincidencia de un respaldo completo.

Ilustración original: compara el SHA-256 y tamaño de un puntero LFS confirmado de confianza con un miembro ZIP exacto.
Ilustración editorial original: compara tamaño descomprimido y SHA-256; distingue punteros, discrepancias y resultados no concluyentes. Watch at Ease

Los ZIP de código fuente de GitHub incluyen punteros Git LFS de forma predeterminada. Los administradores pueden optar por incluir los objetos grandes, pero importan las reglas de seguimiento confirmadas y la ubicación del almacenamiento LFS. Que una descarga se abra correctamente no demuestra que contenga esos datos.

Esta guía comprueba una cuestión más concreta: ¿un archivo exacto del ZIP coincide con el puntero LFS ordinario confirmado en la versión prevista? El verificador Python de solo lectura compara el tamaño descomprimido y SHA-256, informa de punteros y discrepancias y conserva ambas entradas. Incluso MATCH no significa que puedas descartar todo el clon.

1. Distinguir la política del archivo de un problema del clon

La opción Settings → Archives de GitHub permite incluir objetos Git LFS en los archivos de código fuente generados. Las reglas de seguimiento deben estar confirmadas en .gitattributes. Los archivos de un servidor LFS externo configurado en .lfsconfig no se incluyen mediante esta opción.

Estas son condiciones del archivo en el repositorio. Instalar LFS en tu equipo o recuperar un objeto en un clon no modifica un ZIP ya descargado. Un recurso de versión subido por separado también es un artefacto distinto de la instantánea de código que GitHub genera automáticamente.

Conserva el clon y el ZIP originales mientras investigas. La instantánea no guarda todo el historial Git, y esta prueba de un solo miembro no verifica archivos sin confirmar ni otras versiones que puedas necesitar.

2. Fijar la versión y el miembro exactos

Registra el identificador completo del commit previsto y la ruta relativa al repositorio, por ejemplo assets/model.bin. Una rama que cambia o un archivo del árbol de trabajo de una versión posterior son malas referencias para un ZIP antiguo. Si no puedes establecer el commit previsto, el resultado sigue sin verificar.

Lista el ZIP sin extraerlo: python -m zipfile -l source.zip. Copia el nombre exacto del miembro, incluido el prefijo de nivel superior; por ejemplo, project-commit/assets/model.bin. No supongas que el prefijo coincide con la ruta del repositorio.

Mantén intactas las copias de entrada durante la auditoría. El verificador no resuelve rutas renombradas, no elige entre nombres duplicados ni deduce la etiqueta que representa el archivo. La coincidencia de bytes solo es útil si la referencia corresponde a la versión y al archivo previstos.

3. Leer el puntero confirmado, no el contenido del árbol de trabajo

Ejecuta la preparación siguiente dentro del clon conservado. Sustituye FULL_COMMIT, la ruta del repositorio y el destino; el directorio padre debe existir y el archivo debe ser nuevo. git cat-file blob, con el tipo indicado, lee el blob confirmado sin procesar. El auxiliar espera a que Git termine correctamente antes de crear la referencia y usa escritura binaria exclusiva para rechazar la sobrescritura.

```python import subprocess from pathlib import Path blob = subprocess.check_output([ "git", "cat-file", "blob", "FULL_COMMIT:assets/model.bin" ]) with Path("/outside-clone/expected.pointer").open("xb") as output: output.write(blob) ```

Después ejecuta git lfs pointer --check --strict --file=/outside-clone/expected.pointer y comprueba el código de salida. Un puntero canónico válido devuelve 0; un archivo que no es un puntero devuelve 1, y un puntero no canónico bajo comprobación estricta devuelve 2. Un fallo exige revisar la referencia, no editarla hasta que pase.

La referencia debe tener la forma v1 habitual de tres líneas: URL de version, oid sha256 con su resumen completo en minúsculas y size en bytes, con finales de línea LF. El verificador excluye deliberadamente las formas con extensiones y las antiguas. Un blob confirmado vacío tampoco es una referencia ordinaria de tres líneas para esta herramienta.

4. Ejecutar una auditoría limitada a un archivo

Guarda todo el código como lfs_zip_audit.py. Ejecuta python lfs_zip_audit.py /outside-clone/expected.pointer source.zip "project-commit/assets/model.bin". Lee el miembro exacto sin extraerlo, calcula el hash del flujo descomprimido y compara tanto su longitud en bytes como su SHA-256 con la referencia.

El límite de lectura predeterminado por archivo es de 2 GiB. Un archivo mayor queda INCONCLUSIVE salvo que elijas deliberadamente un valor adecuado de --max-bytes y dispongas de recursos para leerlo. El límite de 100,000 miembros se aplica después de analizar el directorio ZIP; no significa que el programa sea un entorno aislado para archivos hostiles.

```python """Read-only single-member ZIP audit against a trusted canonical v1 LFS pointer.""" import argparse import hashlib import json import re import stat import zipfile import zlib from pathlib import Path POINTER = re.compile(rb'version https://git-lfs.github.com/spec/v1\noid sha256:([0-9a-f]{64})\nsize (0|[1-9][0-9]*)\n') def parse_pointer(data): if len(data) >= 1024: raise ValueError('reference is outside the pointer-size scope') match = POINTER.fullmatch(data) if not match: raise ValueError('requires an ordinary canonical three-line v1 pointer; extensions, legacy and noncanonical forms are outside scope') return match[1].decode('ascii'), int(match[2]) def regular(path): path = Path(path) if not stat.S_ISREG(path.lstat().st_mode): raise ValueError('input must be a regular file, not a symlink or directory') return path def audit(pointer_path, archive_path, member_name, max_bytes=2*1024**3): if max_bytes < 0: raise ValueError('max_bytes must be nonnegative') pointer_path, archive_path = regular(pointer_path), regular(archive_path) with pointer_path.open('rb') as stream: reference = stream.read(1024) expected_sha, expected_size = parse_pointer(reference) if expected_size > max_bytes: raise ValueError('expected payload exceeds the selected read budget') parts = member_name.split('/') if not member_name or '\\' in member_name or '\x00' in member_name or any(p in ('', '.', '..') for p in parts): raise ValueError('use the exact safe slash-separated member name including its archive prefix') with zipfile.ZipFile(archive_path) as archive: infos = archive.infolist() if len(infos) > 100000: raise ValueError('too many ZIP members for this audit scope') matches = [i for i in infos if i.filename == member_name] if not matches: return {'status':'MISSING_MEMBER','member':member_name} if len(matches) != 1: raise ValueError('duplicate exact member name; no unambiguous selection') info = matches[0] mode = stat.S_IFMT(info.external_attr >> 16) if info.is_dir() or mode not in (0, stat.S_IFREG): raise ValueError('member must be a regular file, not a link or directory') if info.flag_bits & 1: raise ValueError('encrypted ZIP members are outside scope') if info.file_size > max_bytes: raise ValueError('ZIP member exceeds the selected read budget') digest, size, prefix = hashlib.sha256(), 0, bytearray() with archive.open(info, 'r') as stream: while True: block = stream.read(1024*1024) if not block: break size += len(block) if size > max_bytes: raise ValueError('stream exceeded the selected read budget') digest.update(block) if len(prefix) < 1024: prefix.extend(block[:1024-len(prefix)]) actual_sha = digest.hexdigest() if size == expected_size and actual_sha == expected_sha: status = 'MATCH' else: try: found = parse_pointer(bytes(prefix)) if size < 1024 else None except ValueError: found = None if found == (expected_sha, expected_size): status = 'POINTER_ONLY' elif found: status = 'OTHER_POINTER' else: status = 'SIZE_MISMATCH' if size != expected_size else 'DIGEST_MISMATCH' return {'status':status,'member':member_name,'expectedBytes':expected_size,'actualBytes':size,'expectedSha256':expected_sha,'actualSha256':actual_sha} def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('pointer');parser.add_argument('zip');parser.add_argument('member') parser.add_argument('--max-bytes',type=int,default=2*1024**3) args = parser.parse_args() try: result = audit(args.pointer,args.zip,args.member,args.max_bytes) except (OSError,ValueError,RuntimeError,EOFError,zlib.error,zipfile.BadZipFile,NotImplementedError) as exc: result = {'status':'INCONCLUSIVE','reason':str(exc)} print(json.dumps(result,ensure_ascii=False)) return 0 if result['status']=='MATCH' else 2 if result['status']=='INCONCLUSIVE' else 1 if __name__=='__main__': raise SystemExit(main()) ```

El verificador no realiza conexiones de red, descargas, operaciones de checkout, escrituras en los archivos de entrada ni eliminaciones. Imprime un resultado JSON en la salida estándar. Si guardas el informe, elige un destino nuevo fuera de las entradas y no publiques rutas privadas ni identificadores de objetos en solicitudes de soporte.

5. Interpretar todos los resultados antes de actuar

MATCH significa que el tamaño descomprimido y el resumen del miembro coinciden con el puntero de confianza. Demuestra la identidad de bytes del contenido seleccionado dentro del alcance indicado. No demuestra que todo el archivo esté completo, que la referencia sea auténtica ni que el archivo sea seguro de ejecutar.

POINTER_ONLY indica que el miembro es el mismo puntero ordinario, no el contenido esperado. OTHER_POINTER identifica otro objeto. MISSING_MEMBER indica que falta el nombre exacto; vuelve a comprobar primero el prefijo y la versión prevista.

SIZE_MISMATCH y DIGEST_MISMATCH distinguen los fallos de longitud de los de contenido. INCONCLUSIVE incluye referencias no admitidas, nombres duplicados, enlaces, cifrado, límites de lectura y errores al leer el ZIP. No es un éxito sin errores ni una orden de forzar la extracción.

Resultado | Qué queda establecido | Próximo paso seguro MATCH | Un miembro coincide con el OID y tamaño de confianza | Probarlo en la aplicación prevista y ampliar la cobertura por separado POINTER_ONLY | El ZIP todavía contiene el puntero esperado | Revisar la política de inclusión o conseguir el contenido exacto mediante la vía LFS normal y autorizada OTHER_POINTER | Se referencia un objeto distinto | Conciliar commit, ruta y versión del archivo antes de recuperar datos MISSING_MEMBER | No se encontró el nombre exacto | Revisar prefijo y versión; no sustituirlo silenciosamente por otro archivo del mismo nombre SIZE_MISMATCH / DIGEST_MISMATCH | Los bytes no coinciden con la referencia | Conservar ambas entradas y resolver el fallo de identidad o integridad INCONCLUSIVE | No hay un resultado admitido e inequívoco | Resolver el límite indicado y conservar las entradas y el clon

6. Por qué no bastan el tamaño y el texto que parece un puntero

Dos contenidos pueden tener la misma longitud y ser diferentes. El nombre y el tamaño no sustituyen la identidad SHA-256 del puntero. El verificador clasifica un contenido erróneo de igual tamaño como DIGEST_MISMATCH.

A la inversa, el contenido puede ser literalmente un texto parecido a un puntero. El programa comprueba primero el tamaño y el resumen esperados antes de clasificar un miembro de texto pequeño como puntero. Por ello, un contenido de texto de puntero con la identidad esperada también recibe MATCH.

Las extensiones LFS pueden transformar los bytes almacenados durante clean y smudge. En ese caso, el OID principal puede identificar los bytes transformados en almacenamiento, no los finales del árbol de trabajo. Esta herramienta para punteros ordinarios se detiene ante extensiones en vez de declarar corrupto un archivo transformado válido.

7. Saber qué se probó realmente

El script exacto de 4,500 bytes, con SHA-256 cad7c90e22c622e8e65d7d50d2a6cb7c119718978e8f00caa4e5523aa8e9a460, superó 36 comprobaciones sintéticas en Python 3.12.14/Linux. Se probaron bytes correctos, puntero solo y puntero distinto, fallos de tamaño y resumen, miembros Unicode, prefijos incorrectos, nombres duplicados, enlaces de entrada y de miembro, referencias no admitidas, límites, corrupción CRC y rechazo de miembros cifrados.

Otras pruebas locales de preparación verificaron la extracción del puntero confirmado sin procesar, la creación exclusiva de la referencia, el rechazo de sobrescritura y la comprobación canónica con Git LFS 3.4.1. Los bytes de entrada permanecieron idénticos tras las auditorías de solo lectura. Se usaron datos sintéticos temporales: no se probaron tu repositorio, cuota en la nube ni configuración del archivo.

Para una entrega real, conserva el commit previsto, la ruta del repositorio, el nombre exacto del miembro, los resúmenes y tamaños de referencia y reales, el estado de auditoría y la prueba de apertura en la aplicación. Una cobertura incompleta o un entorno distinto siguen siendo tareas pendientes de verificación.

8. Separar la verificación de un archivo del respaldo completo

Verificar un modelo o archivo multimedia es un resultado útil. Antes de eliminar cualquier clon, revisa por separado todos los archivos necesarios, el historial y las refs de Git, los submódulos, los cambios locales y las versiones fuera de esta instantánea. Un ZIP de código fuente y un clon tienen alcances de conservación diferentes.

Si el ZIP solo contiene punteros, la opción oficial de archivos puede explicar el motivo. Si el objeto no está disponible mediante la vía LFS habitual, conserva cualquier copia local que sepas que funciona y resuelve el acceso o la disponibilidad con el propietario del repositorio. Descargar repetidamente la misma instantánea con punteros no recupera su contenido.

Preguntas frecuentes

¿Activar Include Git LFS objects arregla mi ZIP antiguo? No. Evalúa un archivo recién generado y las condiciones de seguimiento y almacenamiento aplicables. Los bytes del ZIP ya descargado no cambian.

¿git lfs fetch cambia los archivos dentro de un ZIP existente? No. Recuperar datos en un clon y verificar un miembro del archivo son operaciones distintas.

¿Puedo usar un puntero copiado de otra rama? Solo si estableces de forma independiente que identifica el contenido exacto previsto. Prefiere el puntero del commit completo seleccionado y registra esa identidad.

¿Un MATCH me permite borrar el clon? No. Demuestra los bytes de un miembro, no el historial, los demás objetos LFS, el trabajo local ni un plan de recuperación completo.

¿Qué pasa si el puntero tiene líneas de extensión? Detén este verificador. La verificación con extensiones necesita las transformaciones aplicables y sus identidades; no elimines las líneas para forzar una coincidencia.

Ilustración original: un MATCH de un archivo no verifica historial, refs, submódulos ni trabajo local.
Ilustración editorial original: conserva el clon hasta verificar una preservación más amplia; MATCH solo demuestra los bytes del miembro elegido. Watch at Ease