Repository: parse-community/Parse-SDK-JS
Pull request #1467 (2022-03-29 17:05:31)

fix: creating a Parse.File with base64 string fails for some file types
### New Pull Request Checklist
<!--
    Check every following box [x] before submitting your PR.
    Click the "Preview" tab for better readability.
    Thanks for contributing to Parse Platform!
-->

- [x] I am not disclosing a [vulnerability](https://github.com/parse-community/parse-server/blob/master/SECURITY.md).
- [x] I am creating this PR in reference to an [issue](https://github.com/parse-community/Parse-SDK-JS/issues?q=is%3Aissue).

### Issue Description
<!-- Add a brief description of the issue this PR solves. -->

Related issue: [#1466](https://github.com/parse-community/Parse-SDK-JS/issues/1466)

### Approach
<!-- Add a description of the approach in this PR. -->
Reworked logic for base64 formatting in ParseFile.js.

### TODOs before merging
<!--
    Add TODOs that need to be completed before merging this PR.
    Delete TODOs that do not apply to this PR.
-->

- [x] Add tests
- [x] Add entry to changelog

mtrezza: Can you adapt the code to pass? We won't change the node engine version at this point. Node 14 is still under LTS.
parse-github-assistant[bot]: I will reformat the title to use the proper commit message syntax.
RazvanCristian: > Tests still fail, could you take a look? And please let me know if the new PR title is correct.

Sorry for taking so long, I had some work to do. 
I updated the PR and checked that it passes the tests with version `v14.19.1`. It passes all the tests and there should be no other problems.
mtrezza: I changes the PR title because the previous one was difficult to read. It seems you changed it back. What what's incorrect or missing with the title?
codecov[bot]: # [Codecov](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/1467?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=parse-community) Report
> Merging [#1467](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/1467?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=parse-community) (02b9a79) into [alpha](https://codecov.io/gh/parse-community/Parse-SDK-JS/commit/fd22644c11ce8ecd3105e14f7e3b7943e7d731b3?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=parse-community) (fd22644) will **decrease** coverage by `0.01%`.
> The diff coverage is `90.00%`.

```diff
@@            Coverage Diff             @@
##            alpha    #1467      +/-   ##
==========================================
- Coverage   99.94%   99.93%   -0.02%     
==========================================
  Files          61       61              
  Lines        5948     5949       +1     
  Branches     1354     1357       +3     
==========================================
  Hits         5945     5945              
- Misses          3        4       +1     
```


| [Impacted Files](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/1467?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=parse-community) | Coverage Δ | |
|---|---|---|
| [src/ParseFile.js](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/1467/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=parse-community#diff-c3JjL1BhcnNlRmlsZS5qcw==) | `99.57% <90.00%> (-0.43%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/1467?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=parse-community).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=parse-community)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/1467?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=parse-community). Last update [fd22644...02b9a79](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/1467?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=parse-community). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=parse-community).

mtrezza: @rubenatorio Do you want to do a final review?

Patch:
diff --git a/src/ParseFile.js b/src/ParseFile.js
index 9470b48..0cc4e7d 100644
--- a/src/ParseFile.js
+++ b/src/ParseFile.js
@@ -42,7 +42,15 @@ export type FileSource =
       type: string,
     };
 
-const dataUriRegexp = /^data:([a-zA-Z]+\/[-a-zA-Z0-9+.]+)(;charset=[a-zA-Z0-9\-\/]*)?;base64,/;
+const base64Regex = new RegExp(
+  '([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))',
+  'i'
+);
+
+const dataUriRegex = new RegExp(
+  `^data:([a-zA-Z]+\\/[-a-zA-Z0-9+.]+(;[a-z-]+=[a-zA-Z0-9+.-]+)?)?(;base64)?,(${base64Regex.source})*$`,
+  'i'
+);
 
 function b64Digit(number: number): string {
   if (number < 26) {
@@ -137,26 +145,30 @@ class ParseFile {
           type: specifiedType,
         };
       } else if (data && typeof data.base64 === 'string') {
-        const base64 = data.base64;
-        const commaIndex = base64.indexOf(',');
-
-        if (commaIndex !== -1) {
-          const matches = dataUriRegexp.exec(base64.slice(0, commaIndex + 1));
-          // if data URI with type and charset, there will be 4 matches.
-          this._data = base64.slice(commaIndex + 1);
-          this._source = {
-            format: 'base64',
-            base64: this._data,
-            type: matches[1],
-          };
-        } else {
-          this._data = base64;
-          this._source = {
-            format: 'base64',
-            base64: base64,
-            type: specifiedType,
-          };
+        // Check if data URI or base64 string is valid
+        const validationRegex = new RegExp(base64Regex.source + '|' + dataUriRegex.source, 'i');
+        if (!validationRegex.test(data.base64)) {
+          throw new Error(
+            'Cannot create a Parse.File without valid data URIs or base64 encoded data.'
+          );
+        }
+
+        const base64 = data.base64.split(',').slice(-1)[0];
+        let type =
+          specifiedType || data.base64.split(';').slice(0, 1)[0].split(':').slice(1, 2)[0] || '';
+
+        // https://tools.ietf.org/html/rfc2397
+        // If <mediatype> is omitted, it defaults to text/plain;charset=US-ASCII.
+        // As a shorthand, "text/plain" can be omitted but the charset parameter supplied.
+        if (dataUriRegex.test(data.base64)) {
+          type = type || 'text/plain';
         }
+
+        this._source = {
+          format: 'base64',
+          base64,
+          type,
+        };
       } else {
         throw new TypeError('Cannot create a Parse.File with that data.');
       }


Test patch:
diff --git a/integration/test/ParseFileTest.js b/integration/test/ParseFileTest.js
index 1cecbb0..c594ff3 100644
--- a/integration/test/ParseFileTest.js
+++ b/integration/test/ParseFileTest.js
@@ -66,6 +66,7 @@ describe('Parse.File', () => {
 
   it('can get file data from base64', async () => {
     const file = new Parse.File('parse-server-logo', { base64: 'ParseA==' });
+    await file.save();
     let data = await file.getData();
     assert.equal(data, 'ParseA==');
     file._data = null;
@@ -79,6 +80,7 @@ describe('Parse.File', () => {
     const file = new Parse.File('parse-server-logo', {
       base64: 'data:image/jpeg;base64,ParseA==',
     });
+    await file.save();
     let data = await file.getData();
     assert.equal(data, 'ParseA==');
     file._data = null;
diff --git a/src/__tests__/ParseFile-test.js b/src/__tests__/ParseFile-test.js
index 631a6c1..fb045db 100644
--- a/src/__tests__/ParseFile-test.js
+++ b/src/__tests__/ParseFile-test.js
@@ -66,14 +66,30 @@ describe('ParseFile', () => {
     expect(file._source.type).toBe('');
   });
 
-  it('can extract data type from base64', () => {
+  it('can set the default type to be text/plain when using base64', () => {
     const file = new ParseFile('parse.txt', {
+      base64: 'data:;base64,ParseA==',
+    });
+    expect(file._source.base64).toBe('ParseA==');
+    expect(file._source.type).toBe('text/plain');
+  });
+
+  it('can extract data type from base64', () => {
+    const file = new ParseFile('parse.png', {
       base64: 'data:image/png;base64,ParseA==',
     });
     expect(file._source.base64).toBe('ParseA==');
     expect(file._source.type).toBe('image/png');
   });
 
+  it('can extract data type from base64 with a filename parameter', () => {
+    const file = new ParseFile('parse.pdf', {
+      base64: 'data:application/pdf;filename=parse.pdf;base64,ParseA==',
+    });
+    expect(file._source.base64).toBe('ParseA==');
+    expect(file._source.type).toBe('application/pdf');
+  });
+
   it('can create files with file uri', () => {
     const file = new ParseFile('parse-image', {
       uri: 'http://example.com/image.png',
@@ -136,6 +152,12 @@ describe('ParseFile', () => {
     expect(function () {
       new ParseFile('parse.txt', 'string');
     }).toThrow('Cannot create a Parse.File with that data.');
+
+    expect(function () {
+      new ParseFile('parse.txt', {
+        base64: 'abc',
+      });
+    }).toThrow('Cannot create a Parse.File without valid data URIs or base64 encoded data.');
   });
 
   it('throws with invalid base64', () => {