1use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
7#[serde(rename_all = "snake_case")]
8pub enum ContentType {
9 File,
11 Dir,
13 Symlink,
15 Submodule,
17}
18
19impl std::fmt::Display for ContentType {
20 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
21 match self {
22 Self::File => write!(f, "file"),
23 Self::Dir => write!(f, "dir"),
24 Self::Symlink => write!(f, "symlink"),
25 Self::Submodule => write!(f, "submodule"),
26 }
27 }
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct ContentEntry {
33 #[serde(rename = "type")]
35 pub content_type: ContentType,
36 #[serde(skip_serializing_if = "Option::is_none")]
38 pub encoding: Option<String>,
39 pub size: u64,
41 pub name: String,
43 pub path: String,
45 #[serde(skip_serializing_if = "Option::is_none")]
47 pub content: Option<String>,
48 pub sha: String,
50 #[serde(skip_serializing_if = "Option::is_none")]
52 pub download_url: Option<String>,
53 #[serde(skip_serializing_if = "Option::is_none")]
55 pub html_url: Option<String>,
56 #[serde(skip_serializing_if = "Option::is_none")]
58 pub url: Option<String>,
59 #[serde(skip_serializing_if = "Option::is_none")]
61 pub target: Option<String>,
62 #[serde(skip_serializing_if = "Option::is_none")]
64 pub submodule_git_url: Option<String>,
65}
66
67impl ContentEntry {
68 pub fn file(name: String, path: String, sha: String, size: u64) -> Self {
70 Self {
71 content_type: ContentType::File,
72 encoding: None,
73 size,
74 name,
75 path,
76 content: None,
77 sha,
78 download_url: None,
79 html_url: None,
80 url: None,
81 target: None,
82 submodule_git_url: None,
83 }
84 }
85
86 pub fn dir(name: String, path: String, sha: String) -> Self {
88 Self {
89 content_type: ContentType::Dir,
90 encoding: None,
91 size: 0,
92 name,
93 path,
94 content: None,
95 sha,
96 download_url: None,
97 html_url: None,
98 url: None,
99 target: None,
100 submodule_git_url: None,
101 }
102 }
103
104 pub fn symlink(name: String, path: String, sha: String, target: String) -> Self {
106 Self {
107 content_type: ContentType::Symlink,
108 encoding: None,
109 size: target.len() as u64,
110 name,
111 path,
112 content: None,
113 sha,
114 download_url: None,
115 html_url: None,
116 url: None,
117 target: Some(target),
118 submodule_git_url: None,
119 }
120 }
121
122 pub fn submodule(name: String, path: String, sha: String, git_url: String) -> Self {
124 Self {
125 content_type: ContentType::Submodule,
126 encoding: None,
127 size: 0,
128 name,
129 path,
130 content: None,
131 sha,
132 download_url: None,
133 html_url: None,
134 url: None,
135 target: None,
136 submodule_git_url: Some(git_url),
137 }
138 }
139
140 pub fn with_content(mut self, content: String) -> Self {
142 self.encoding = Some("base64".to_string());
143 self.content = Some(content);
144 self
145 }
146
147 pub fn with_urls(mut self, download_url: String, html_url: String, api_url: String) -> Self {
149 self.download_url = Some(download_url);
150 self.html_url = Some(html_url);
151 self.url = Some(api_url);
152 self
153 }
154}
155
156#[derive(Debug, Clone, Serialize, Deserialize)]
158pub struct ReadmeResponse {
159 #[serde(rename = "type")]
161 pub content_type: ContentType,
162 pub encoding: String,
164 pub size: u64,
166 pub name: String,
168 pub path: String,
170 pub content: String,
172 pub sha: String,
174 #[serde(skip_serializing_if = "Option::is_none")]
176 pub download_url: Option<String>,
177 #[serde(skip_serializing_if = "Option::is_none")]
179 pub html_url: Option<String>,
180}
181
182#[derive(Debug, Clone, Serialize, Deserialize)]
184pub struct LicenseResponse {
185 pub name: String,
187 pub path: String,
189 #[serde(skip_serializing_if = "Option::is_none")]
191 pub spdx_id: Option<String>,
192 pub sha: String,
194 pub size: u64,
196 #[serde(skip_serializing_if = "Option::is_none")]
198 pub download_url: Option<String>,
199 #[serde(skip_serializing_if = "Option::is_none")]
201 pub html_url: Option<String>,
202 #[serde(skip_serializing_if = "Option::is_none")]
204 pub content: Option<String>,
205 #[serde(skip_serializing_if = "Option::is_none")]
207 pub encoding: Option<String>,
208}
209
210pub fn recognize_license_file(filename: &str) -> Option<&'static str> {
212 let lower = filename.to_lowercase();
213 if lower == "license" || lower == "license.txt" || lower == "license.md" {
214 Some("LICENSE")
215 } else if lower == "copying" || lower == "copying.txt" {
216 Some("COPYING")
217 } else if lower == "unlicense" {
218 Some("UNLICENSE")
219 } else {
220 None
221 }
222}
223
224pub fn detect_spdx_id(content: &str) -> Option<&'static str> {
226 let content_lower = content.to_lowercase();
227
228 if content_lower.contains("mit license")
230 || content_lower.contains("permission is hereby granted, free of charge")
231 {
232 Some("MIT")
233 } else if content_lower.contains("apache license") && content_lower.contains("version 2.0") {
234 Some("Apache-2.0")
235 } else if content_lower.contains("gnu general public license") {
236 if content_lower.contains("version 3") {
237 Some("GPL-3.0")
238 } else if content_lower.contains("version 2") {
239 Some("GPL-2.0")
240 } else {
241 Some("GPL")
242 }
243 } else if content_lower.contains("bsd 3-clause") || content_lower.contains("new bsd license") {
244 Some("BSD-3-Clause")
245 } else if content_lower.contains("bsd 2-clause") || content_lower.contains("simplified bsd") {
246 Some("BSD-2-Clause")
247 } else if content_lower.contains("mozilla public license") && content_lower.contains("2.0") {
248 Some("MPL-2.0")
249 } else if content_lower.contains("isc license") {
250 Some("ISC")
251 } else if content_lower.contains("unlicense") || content_lower.contains("public domain") {
252 Some("Unlicense")
253 } else {
254 None
255 }
256}
257
258pub fn is_readme_file(filename: &str) -> bool {
260 let lower = filename.to_lowercase();
261 lower == "readme"
262 || lower == "readme.md"
263 || lower == "readme.txt"
264 || lower == "readme.rst"
265 || lower == "readme.markdown"
266 || lower == "readme.rdoc"
267 || lower == "readme.org"
268 || lower == "readme.adoc"
269}
270
271#[derive(Debug, Clone, Default, Serialize, Deserialize)]
273pub struct ContentsQuery {
274 #[serde(rename = "ref")]
276 pub git_ref: Option<String>,
277}
278
279pub fn base64_encode(data: &[u8]) -> String {
281 const ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
282
283 let mut result = String::with_capacity(data.len().div_ceil(3) * 4);
284
285 for chunk in data.chunks(3) {
286 let b0 = chunk[0] as usize;
287 let b1 = chunk.get(1).copied().unwrap_or(0) as usize;
288 let b2 = chunk.get(2).copied().unwrap_or(0) as usize;
289
290 result.push(ALPHABET[b0 >> 2] as char);
291 result.push(ALPHABET[((b0 & 0x03) << 4) | (b1 >> 4)] as char);
292
293 if chunk.len() > 1 {
294 result.push(ALPHABET[((b1 & 0x0f) << 2) | (b2 >> 6)] as char);
295 } else {
296 result.push('=');
297 }
298
299 if chunk.len() > 2 {
300 result.push(ALPHABET[b2 & 0x3f] as char);
301 } else {
302 result.push('=');
303 }
304 }
305
306 result
307}
308
309#[cfg(test)]
310mod tests {
311 use super::*;
312
313 #[test]
314 fn test_content_entry_file() {
315 let entry = ContentEntry::file(
316 "main.rs".to_string(),
317 "src/main.rs".to_string(),
318 "abc123".to_string(),
319 1024,
320 );
321
322 assert_eq!(entry.content_type, ContentType::File);
323 assert_eq!(entry.name, "main.rs");
324 assert_eq!(entry.path, "src/main.rs");
325 assert_eq!(entry.size, 1024);
326 }
327
328 #[test]
329 fn test_content_entry_with_content() {
330 let entry = ContentEntry::file(
331 "test.txt".to_string(),
332 "test.txt".to_string(),
333 "abc123".to_string(),
334 11,
335 )
336 .with_content(base64_encode(b"Hello World"));
337
338 assert_eq!(entry.encoding, Some("base64".to_string()));
339 assert!(entry.content.is_some());
340 }
341
342 #[test]
343 fn test_is_readme_file() {
344 assert!(is_readme_file("README"));
345 assert!(is_readme_file("README.md"));
346 assert!(is_readme_file("readme.txt"));
347 assert!(!is_readme_file("main.rs"));
348 assert!(!is_readme_file("READMEE"));
349 }
350
351 #[test]
352 fn test_recognize_license() {
353 assert_eq!(recognize_license_file("LICENSE"), Some("LICENSE"));
354 assert_eq!(recognize_license_file("license.txt"), Some("LICENSE"));
355 assert_eq!(recognize_license_file("COPYING"), Some("COPYING"));
356 assert_eq!(recognize_license_file("main.rs"), None);
357 }
358
359 #[test]
360 fn test_detect_spdx_id() {
361 assert_eq!(
362 detect_spdx_id("MIT License\n\nPermission is hereby granted, free of charge"),
363 Some("MIT")
364 );
365 assert_eq!(
366 detect_spdx_id("Apache License Version 2.0"),
367 Some("Apache-2.0")
368 );
369 assert_eq!(detect_spdx_id("Random text"), None);
370 }
371
372 #[test]
373 fn test_base64_encode() {
374 assert_eq!(base64_encode(b"Hello"), "SGVsbG8=");
375 assert_eq!(base64_encode(b"Hi"), "SGk=");
376 assert_eq!(base64_encode(b"A"), "QQ==");
377 }
378}