77from pathlib import Path
88import urllib
99import urllib .request
10+ import urllib .parse
1011import json
12+ import re
13+
14+ DOCKER_MANIFEST_V2 = "application/vnd.docker.distribution.manifest.v2+json"
15+ DOCKER_MANIFEST_LIST_V2 = "application/vnd.docker.distribution.manifest.list.v2+json"
16+ OCI_MANIFEST_V1 = "application/vnd.oci.image.manifest.v1+json"
17+ OCI_INDEX_V1 = "application/vnd.oci.image.index.v1+json"
18+
19+ SUPPORTED_MANIFEST_TYPES = [
20+ DOCKER_MANIFEST_V2 ,
21+ OCI_MANIFEST_V1 ,
22+ ]
23+
24+ ACCEPT_MANIFEST_TYPES = ", " .join ([
25+ * SUPPORTED_MANIFEST_TYPES ,
26+ DOCKER_MANIFEST_LIST_V2 ,
27+ OCI_INDEX_V1 ,
28+ ])
1129
1230# Default values
1331ddiff_port = os .getenv ("DDIFF_PORT" , "5000" )
@@ -49,18 +67,29 @@ def _request_manifest(tag):
4967 manifest_url = f"{ ddiff_url } /v2/{ repo } /manifests/{ version_tag } "
5068
5169 req = urllib .request .Request (manifest_url )
52- req .add_header ("Accept" , "application/vnd.docker.distribution.manifest.v2+json" )
70+ req .add_header ("Accept" , ACCEPT_MANIFEST_TYPES )
5371 try :
5472 with urllib .request .urlopen (req ) as response :
5573 manifest = response .read ().decode ()
56- return manifest
74+ content_type = (response .getheader ("Content-Type" ) or "" ).split (";" )[0 ]
75+ return manifest , content_type
5776 except urllib .error .HTTPError as e :
5877 print_error (f"HTTP error: { e .code } - { e .reason } ({ manifest_url } )" )
5978 except Exception as e :
6079 print_error (f"Error: { e } ({ manifest_url } )" )
6180
81+ def _validate_manifest_media_type (manifest ):
82+ media_type = manifest .get ("mediaType" , "" )
83+ if media_type in [DOCKER_MANIFEST_LIST_V2 , OCI_INDEX_V1 ]:
84+ print_error ("Manifest list/index is not supported yet. Please provide a single image manifest tag." )
85+ if media_type and media_type not in SUPPORTED_MANIFEST_TYPES :
86+ print_error (f"Unsupported manifest mediaType: { media_type } " )
87+ return media_type
88+
89+
6290def _parse_blob_list (manifest_str ):
6391 manifest = json .loads (manifest_str )
92+ _validate_manifest_media_type (manifest )
6493 digests = []
6594
6695 # Config blob
@@ -75,7 +104,8 @@ def _parse_blob_list(manifest_str):
75104 return digests
76105
77106def _download_blob (repo , digest , output_path ):
78- assert digest .startswith ("sha256:" ) # Digest must start with 'sha256:'
107+ if not re .match (r"^[a-z0-9_+.-]+:[a-fA-F0-9]+$" , digest ):
108+ print_error (f"Invalid digest format: { digest } " )
79109
80110 blob_url = f"{ ddiff_url } /v2/{ repo } /blobs/{ digest } "
81111
@@ -105,7 +135,8 @@ def _upload_blob(repo, digest, blob_dir):
105135 # Load tar file of the layer
106136 with open (f"{ blob_dir } /{ digest } .tar" , 'rb' ) as f :
107137 data = f .read ()
108- full_url = f"{ session_url } &digest={ digest } "
138+ delimiter = "&" if "?" in session_url else "?"
139+ full_url = f"{ session_url } { delimiter } digest={ urllib .parse .quote (digest )} "
109140 req = urllib .request .Request (full_url , data = data , method = "PUT" )
110141 req .add_header ("Content-Type" , "application/octet-stream" )
111142 with urllib .request .urlopen (req ) as res :
@@ -125,14 +156,14 @@ def _cross_mount(target_repo, base_repo, digest):
125156 except urllib .error .HTTPError as e :
126157 print_error (f"Mount failed: { e .code } { e .reason } ({ url } )" )
127158
128- def _upload_manifest (tag , manifest_path ):
159+ def _upload_manifest (tag , manifest_path , manifest_media_type = DOCKER_MANIFEST_V2 ):
129160 repo , version_tag = tag .split (":" )
130161 url = f"{ ddiff_url } /v2/{ repo } /manifests/{ version_tag } "
131162 try :
132163 with open (manifest_path , 'rb' ) as f :
133164 data = f .read ()
134165 req = urllib .request .Request (url , data = data , method = "PUT" )
135- req .add_header ("Content-Type" , "application/vnd.docker.distribution.manifest.v2+json" )
166+ req .add_header ("Content-Type" , manifest_media_type )
136167 with urllib .request .urlopen (req ) as res :
137168 print_debug (f"Manifest uploaded to { repo } (status { res .status } )" )
138169 except urllib .error .HTTPError as e :
@@ -179,8 +210,8 @@ def diff_image(base_tag, target_tag):
179210 os .makedirs (blob_dir )
180211
181212 # Download manifest
182- base_manifest = _request_manifest (base_tag )
183- target_manifest = _request_manifest (target_tag )
213+ base_manifest , _ = _request_manifest (base_tag )
214+ target_manifest , target_manifest_media_type = _request_manifest (target_tag )
184215
185216 # Download different blobs
186217 base_blobs = _parse_blob_list (base_manifest )
@@ -194,6 +225,8 @@ def diff_image(base_tag, target_tag):
194225 # Write back metadata
195226 with open (output_dir + "/manifest.json" , "w" ) as f :
196227 f .write (target_manifest )
228+ with open (os .path .join (output_dir , "MANIFEST_MEDIA_TYPE" ), "w" ) as f :
229+ f .write (target_manifest_media_type )
197230 with open (os .path .join (output_dir , "BASE" ), "w" ) as f :
198231 f .write (base_tag )
199232 with open (os .path .join (output_dir , "TARGET" ), "w" ) as f :
@@ -234,6 +267,12 @@ def load_image(base_tag, image_tarball):
234267 with open (os .path .join (input_dir , "UPLOAD_BLOBS" )) as f :
235268 upload_blobs = f .read ().strip ().split ("|" )
236269
270+ manifest_media_type = DOCKER_MANIFEST_V2
271+ manifest_media_type_path = os .path .join (input_dir , "MANIFEST_MEDIA_TYPE" )
272+ if os .path .exists (manifest_media_type_path ):
273+ with open (manifest_media_type_path ) as f :
274+ manifest_media_type = f .read ().strip () or DOCKER_MANIFEST_V2
275+
237276 # Mount
238277 for blob in mount_blobs :
239278 _cross_mount (target_repo , base_repo , blob )
@@ -242,7 +281,7 @@ def load_image(base_tag, image_tarball):
242281 for blob in upload_blobs :
243282 _upload_blob (target_repo , blob , blob_dir )
244283
245- _upload_manifest (target_tag , f"{ input_dir } /manifest.json" )
284+ _upload_manifest (target_tag , f"{ input_dir } /manifest.json" , manifest_media_type )
246285
247286 shutil .rmtree (input_dir )
248287
@@ -282,7 +321,7 @@ def list_blobs(tag):
282321 tag = _prepare_tag (tag )
283322
284323 # Download manifest
285- manifest = _request_manifest (tag )
324+ manifest , _ = _request_manifest (tag )
286325
287326
288327 # Check blobs
@@ -345,4 +384,3 @@ def list_blobs(tag):
345384 print ("Usage: python3 ddiff.py list <tag>" )
346385 sys .exit (1 )
347386 list_blobs (args [0 ])
348-
0 commit comments