@@ -163,6 +163,104 @@ def test_pull_validates_registered_digest(monkeypatch, tmp_path, algorithm):
163163 assert outfile .read_bytes () == content
164164
165165
166+ def test_download_blob_validates_empty_blob (monkeypatch , tmp_path ):
167+ client = oras .provider .Registry (insecure = True )
168+ response = requests .Response ()
169+ response .status_code = 200
170+ response ._content = b""
171+ response ._content_consumed = True
172+ monkeypatch .setattr (client , "get_blob" , lambda * args , ** kwargs : response )
173+ staged = tmp_path / "staged"
174+ staged .touch ()
175+ monkeypatch .setattr (oras .utils , "get_tmpfile" , lambda : str (staged ))
176+ outfile = tmp_path / "empty"
177+
178+ result = client .download_blob (
179+ "registry.example/repository:tag" ,
180+ oras .defaults .blank_hash ,
181+ str (outfile ),
182+ size = 0 ,
183+ )
184+
185+ assert result == str (outfile )
186+ assert outfile .read_bytes () == b""
187+ assert not staged .exists ()
188+
189+
190+ @pytest .mark .parametrize ("status_code" , [401 , 404 ])
191+ def test_download_empty_blob_propagates_http_error (monkeypatch , tmp_path , status_code ):
192+ client = oras .provider .Registry (insecure = True )
193+ response = requests .Response ()
194+ response .status_code = status_code
195+ response .url = "https://registry.example/v2/repository/blobs/empty"
196+ response ._content = b""
197+ response ._content_consumed = True
198+ monkeypatch .setattr (client , "get_blob" , lambda * args , ** kwargs : response )
199+ staged = tmp_path / "staged"
200+ staged .touch ()
201+ monkeypatch .setattr (oras .utils , "get_tmpfile" , lambda : str (staged ))
202+ outfile = tmp_path / "empty"
203+ outfile .write_bytes (b"existing content" )
204+
205+ with pytest .raises (requests .HTTPError ):
206+ client .download_blob (
207+ "registry.example/repository:tag" ,
208+ oras .defaults .blank_hash ,
209+ str (outfile ),
210+ size = 0 ,
211+ )
212+
213+ assert outfile .read_bytes () == b"existing content"
214+ assert not staged .exists ()
215+
216+
217+ def test_download_empty_blob_propagates_transport_error (monkeypatch , tmp_path ):
218+ client = oras .provider .Registry (insecure = True )
219+
220+ def get_blob (* args , ** kwargs ):
221+ raise requests .ConnectionError ("download failed" )
222+
223+ monkeypatch .setattr (client , "get_blob" , get_blob )
224+ staged = tmp_path / "staged"
225+ staged .touch ()
226+ monkeypatch .setattr (oras .utils , "get_tmpfile" , lambda : str (staged ))
227+ outfile = tmp_path / "empty"
228+
229+ with pytest .raises (requests .ConnectionError , match = "download failed" ):
230+ client .download_blob (
231+ "registry.example/repository:tag" ,
232+ oras .defaults .blank_hash ,
233+ str (outfile ),
234+ size = 0 ,
235+ )
236+
237+ assert not outfile .exists ()
238+ assert not staged .exists ()
239+
240+
241+ def test_download_empty_blob_rejects_nonempty_content (monkeypatch , tmp_path ):
242+ client = oras .provider .Registry (insecure = True )
243+ response = requests .Response ()
244+ response .status_code = 200
245+ response ._content = b"not empty"
246+ response ._content_consumed = True
247+ monkeypatch .setattr (client , "get_blob" , lambda * args , ** kwargs : response )
248+ staged = tmp_path / "staged"
249+ staged .touch ()
250+ monkeypatch .setattr (oras .utils , "get_tmpfile" , lambda : str (staged ))
251+ outfile = tmp_path / "empty"
252+
253+ with pytest .raises (ValueError , match = "Downloaded blob digest mismatch" ):
254+ client .download_blob (
255+ "registry.example/repository:tag" ,
256+ oras .defaults .blank_hash ,
257+ str (outfile ),
258+ )
259+
260+ assert not outfile .exists ()
261+ assert not staged .exists ()
262+
263+
166264def test_pull_rejects_digest_mismatch_without_replacing_file (monkeypatch , tmp_path ):
167265 expected_content = b"expected content"
168266 downloaded_content = b"corrupt! content"
0 commit comments